atagen

jelq

public

an http based roguelike rpg about jelqing

Star0
Watch0
7d3fb63208bf5064f04782aac0c0273305f8f35f
1
ssh://brewery@pls.lobotomise.me:33/atagen/jelq.git

Blame: src/tests.rs

782069860236atagen2026-10-10 12:44 UTC1
use std::{sync::Arc, time::Duration};
a9b016e4b248atagen2026-10-10 08:12 UTC2
a9b016e4b248atagen2026-10-10 08:12 UTC3
use axum::{
a9b016e4b248atagen2026-10-10 08:12 UTC4
    Router,
a9b016e4b248atagen2026-10-10 08:12 UTC5
    body::{Body, to_bytes},
782069860236atagen2026-10-10 12:44 UTC6
    http::{Method, Request, StatusCode, Version, header},
a9b016e4b248atagen2026-10-10 08:12 UTC7
    response::Response,
a9b016e4b248atagen2026-10-10 08:12 UTC8
};
a9b016e4b248atagen2026-10-10 08:12 UTC9
use serde_json::Value;
a9b016e4b248atagen2026-10-10 08:12 UTC10
use tower::ServiceExt;
a9b016e4b248atagen2026-10-10 08:12 UTC11
782069860236atagen2026-10-10 12:44 UTC12
use crate::{
782069860236atagen2026-10-10 12:44 UTC13
    AppState, Config,
d46f9d0b953aatagen2026-10-10 14:41 UTC14
    game::{Floor, MumVisit, Run, Story, balance::SUSPICION_LIMIT},
782069860236atagen2026-10-10 12:44 UTC15
    router,
782069860236atagen2026-10-10 12:44 UTC16
};
782069860236atagen2026-10-10 12:44 UTC17
a9b016e4b248atagen2026-10-10 08:12 UTC18
async fn send(
a9b016e4b248atagen2026-10-10 08:12 UTC19
    app: &Router,
a9b016e4b248atagen2026-10-10 08:12 UTC20
    method: &str,
a9b016e4b248atagen2026-10-10 08:12 UTC21
    uri: &str,
a9b016e4b248atagen2026-10-10 08:12 UTC22
    headers: &[(&str, &str)],
a9b016e4b248atagen2026-10-10 08:12 UTC23
    body: &str,
a9b016e4b248atagen2026-10-10 08:12 UTC24
) -> Response {
a9b016e4b248atagen2026-10-10 08:12 UTC25
    let mut request = Request::builder()
a9b016e4b248atagen2026-10-10 08:12 UTC26
        .method(Method::from_bytes(method.as_bytes()).unwrap())
a9b016e4b248atagen2026-10-10 08:12 UTC27
        .uri(uri);
a9b016e4b248atagen2026-10-10 08:12 UTC28
    for (name, value) in headers {
a9b016e4b248atagen2026-10-10 08:12 UTC29
        request = request.header(*name, *value);
a9b016e4b248atagen2026-10-10 08:12 UTC30
    }
a9b016e4b248atagen2026-10-10 08:12 UTC31
    app.clone()
a9b016e4b248atagen2026-10-10 08:12 UTC32
        .oneshot(request.body(Body::from(body.to_owned())).unwrap())
a9b016e4b248atagen2026-10-10 08:12 UTC33
        .await
a9b016e4b248atagen2026-10-10 08:12 UTC34
        .unwrap()
a9b016e4b248atagen2026-10-10 08:12 UTC35
}
a9b016e4b248atagen2026-10-10 08:12 UTC36
a9b016e4b248atagen2026-10-10 08:12 UTC37
async fn json(response: Response) -> Value {
a9b016e4b248atagen2026-10-10 08:12 UTC38
    serde_json::from_slice(&to_bytes(response.into_body(), usize::MAX).await.unwrap()).unwrap()
a9b016e4b248atagen2026-10-10 08:12 UTC39
}
a9b016e4b248atagen2026-10-10 08:12 UTC40
782069860236atagen2026-10-10 12:44 UTC41
async fn text(response: Response) -> String {
782069860236atagen2026-10-10 12:44 UTC42
    String::from_utf8(
782069860236atagen2026-10-10 12:44 UTC43
        to_bytes(response.into_body(), usize::MAX)
782069860236atagen2026-10-10 12:44 UTC44
            .await
782069860236atagen2026-10-10 12:44 UTC45
            .unwrap()
782069860236atagen2026-10-10 12:44 UTC46
            .to_vec(),
782069860236atagen2026-10-10 12:44 UTC47
    )
782069860236atagen2026-10-10 12:44 UTC48
    .unwrap()
782069860236atagen2026-10-10 12:44 UTC49
}
782069860236atagen2026-10-10 12:44 UTC50
782069860236atagen2026-10-10 12:44 UTC51
fn header<'a>(response: &'a Response, name: &str) -> &'a str {
782069860236atagen2026-10-10 12:44 UTC52
    response.headers()[name].to_str().unwrap()
782069860236atagen2026-10-10 12:44 UTC53
}
782069860236atagen2026-10-10 12:44 UTC54
782069860236atagen2026-10-10 12:44 UTC55
fn fixture() -> Run {
782069860236atagen2026-10-10 12:44 UTC56
    Run::new("cafebabe".into(), "cafe".into(), 69)
782069860236atagen2026-10-10 12:44 UTC57
}
782069860236atagen2026-10-10 12:44 UTC58
782069860236atagen2026-10-10 12:44 UTC59
fn seeded_with(floor: Floor, setup: impl FnOnce(&mut Run)) -> Router {
782069860236atagen2026-10-10 12:44 UTC60
    let state = Arc::new(AppState::default());
782069860236atagen2026-10-10 12:44 UTC61
    let mut run = fixture();
782069860236atagen2026-10-10 12:44 UTC62
    run.floor = floor;
782069860236atagen2026-10-10 12:44 UTC63
    setup(&mut run);
782069860236atagen2026-10-10 12:44 UTC64
    state.insert(run);
782069860236atagen2026-10-10 12:44 UTC65
    router(state)
782069860236atagen2026-10-10 12:44 UTC66
}
782069860236atagen2026-10-10 12:44 UTC67
dfa38cf4cd4batagen2026-10-10 09:17 UTC68
fn seeded(floor: Floor) -> (Router, String) {
a9b016e4b248atagen2026-10-10 08:12 UTC69
    let state = Arc::new(AppState::default());
782069860236atagen2026-10-10 12:44 UTC70
    let mut run = fixture();
dfa38cf4cd4batagen2026-10-10 09:17 UTC71
    run.floor = floor;
782069860236atagen2026-10-10 12:44 UTC72
    let etag = run.origin_etag();
a9b016e4b248atagen2026-10-10 08:12 UTC73
    state.insert(run);
a9b016e4b248atagen2026-10-10 08:12 UTC74
    (router(state), etag)
a9b016e4b248atagen2026-10-10 08:12 UTC75
}
a9b016e4b248atagen2026-10-10 08:12 UTC76
782069860236atagen2026-10-10 12:44 UTC77
fn app() -> Router {
782069860236atagen2026-10-10 12:44 UTC78
    crate::app(Config::default())
782069860236atagen2026-10-10 12:44 UTC79
}
782069860236atagen2026-10-10 12:44 UTC80
9683736a2c5eatagen2026-10-10 13:15 UTC81
fn mum_at_the_door(run: &mut Run) {
9683736a2c5eatagen2026-10-10 13:15 UTC82
    run.mum = Some(MumVisit {
782069860236atagen2026-10-10 12:44 UTC83
        errand: "She heard a noise.",
782069860236atagen2026-10-10 12:44 UTC84
        nonce: "abc".into(),
782069860236atagen2026-10-10 12:44 UTC85
        strikes: 0,
0d604ee39751atagen2026-10-10 14:15 UTC86
        knocked: false,
782069860236atagen2026-10-10 12:44 UTC87
    });
782069860236atagen2026-10-10 12:44 UTC88
}
782069860236atagen2026-10-10 12:44 UTC89
a9b016e4b248atagen2026-10-10 08:12 UTC90
#[tokio::test]
a9b016e4b248atagen2026-10-10 08:12 UTC91
async fn creating_a_run_returns_location_and_etag() {
782069860236atagen2026-10-10 12:44 UTC92
    let response = send(&app(), "POST", "/runs?seed=1", &[], "").await;
a9b016e4b248atagen2026-10-10 08:12 UTC93
    assert_eq!(response.status(), StatusCode::CREATED);
782069860236atagen2026-10-10 12:44 UTC94
    assert!(header(&response, "location").starts_with("/runs/"));
a9b016e4b248atagen2026-10-10 08:12 UTC95
    assert!(response.headers().contains_key(header::ETAG));
782069860236atagen2026-10-10 12:44 UTC96
    assert_eq!(header(&response, "server"), "jelqd/0.6.9");
782069860236atagen2026-10-10 12:44 UTC97
}
782069860236atagen2026-10-10 12:44 UTC98
782069860236atagen2026-10-10 12:44 UTC99
#[tokio::test]
782069860236atagen2026-10-10 12:44 UTC100
async fn bodies_do_not_repeat_the_headers() {
782069860236atagen2026-10-10 12:44 UTC101
    let (app, _) = seeded(Floor::Bedroom);
782069860236atagen2026-10-10 12:44 UTC102
    let response = send(&app, "GET", "/runs/cafebabe", &[], "").await;
782069860236atagen2026-10-10 12:44 UTC103
    assert!(response.headers().contains_key("x-eq"));
782069860236atagen2026-10-10 12:44 UTC104
    let body = json(response).await;
782069860236atagen2026-10-10 12:44 UTC105
    for stat in [
782069860236atagen2026-10-10 12:44 UTC106
        "eq",
782069860236atagen2026-10-10 12:44 UTC107
        "girth",
782069860236atagen2026-10-10 12:44 UTC108
        "foreskin",
782069860236atagen2026-10-10 12:44 UTC109
        "stamina",
782069860236atagen2026-10-10 12:44 UTC110
        "cash",
782069860236atagen2026-10-10 12:44 UTC111
        "self_reported_length",
782069860236atagen2026-10-10 12:44 UTC112
    ] {
782069860236atagen2026-10-10 12:44 UTC113
        assert!(body.get(stat).is_none(), "{stat} is already a header");
782069860236atagen2026-10-10 12:44 UTC114
    }
a9b016e4b248atagen2026-10-10 08:12 UTC115
}
a9b016e4b248atagen2026-10-10 08:12 UTC116
a9b016e4b248atagen2026-10-10 08:12 UTC117
#[tokio::test]
a9b016e4b248atagen2026-10-10 08:12 UTC118
async fn jelqing_cold_fails_the_precondition() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC119
    let (app, _) = seeded(Floor::Bedroom);
a9b016e4b248atagen2026-10-10 08:12 UTC120
    let response = send(&app, "JELQ", "/runs/cafebabe?technique=stretch", &[], "").await;
a9b016e4b248atagen2026-10-10 08:12 UTC121
    assert_eq!(response.status(), StatusCode::PRECONDITION_FAILED);
a9b016e4b248atagen2026-10-10 08:12 UTC122
    assert_eq!(
782069860236atagen2026-10-10 12:44 UTC123
        header(&response, "content-type"),
a9b016e4b248atagen2026-10-10 08:12 UTC124
        "application/problem+json"
a9b016e4b248atagen2026-10-10 08:12 UTC125
    );
a9b016e4b248atagen2026-10-10 08:12 UTC126
}
a9b016e4b248atagen2026-10-10 08:12 UTC127
a9b016e4b248atagen2026-10-10 08:12 UTC128
#[tokio::test]
a9b016e4b248atagen2026-10-10 08:12 UTC129
async fn wet_without_lube_is_unsupported_media() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC130
    let (app, _) = seeded(Floor::Bedroom);
a9b016e4b248atagen2026-10-10 08:12 UTC131
    send(&app, "JELQ", "/runs/cafebabe?technique=warmup", &[], "").await;
a9b016e4b248atagen2026-10-10 08:12 UTC132
    let response = send(&app, "JELQ", "/runs/cafebabe?technique=wet", &[], "").await;
a9b016e4b248atagen2026-10-10 08:12 UTC133
    assert_eq!(response.status(), StatusCode::UNSUPPORTED_MEDIA_TYPE);
a9b016e4b248atagen2026-10-10 08:12 UTC134
    let images = send(
a9b016e4b248atagen2026-10-10 08:12 UTC135
        &app,
a9b016e4b248atagen2026-10-10 08:12 UTC136
        "JELQ",
a9b016e4b248atagen2026-10-10 08:12 UTC137
        "/runs/cafebabe?technique=wet",
a9b016e4b248atagen2026-10-10 08:12 UTC138
        &[("content-type", "image/jpeg")],
a9b016e4b248atagen2026-10-10 08:12 UTC139
        "",
a9b016e4b248atagen2026-10-10 08:12 UTC140
    )
a9b016e4b248atagen2026-10-10 08:12 UTC141
    .await;
a9b016e4b248atagen2026-10-10 08:12 UTC142
    assert_eq!(json(images).await["detail"], "No.");
a9b016e4b248atagen2026-10-10 08:12 UTC143
}
a9b016e4b248atagen2026-10-10 08:12 UTC144
a9b016e4b248atagen2026-10-10 08:12 UTC145
#[tokio::test]
a9b016e4b248atagen2026-10-10 08:12 UTC146
async fn unknown_methods_are_not_implemented() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC147
    let (app, _) = seeded(Floor::Bedroom);
a9b016e4b248atagen2026-10-10 08:12 UTC148
    let response = send(&app, "MILK", "/runs/cafebabe", &[], "").await;
a9b016e4b248atagen2026-10-10 08:12 UTC149
    assert_eq!(response.status(), StatusCode::NOT_IMPLEMENTED);
782069860236atagen2026-10-10 12:44 UTC150
    let teapot = send(&app, "BREW", "/runs/cafebabe", &[], "").await;
782069860236atagen2026-10-10 12:44 UTC151
    assert_eq!(teapot.status(), StatusCode::IM_A_TEAPOT);
782069860236atagen2026-10-10 12:44 UTC152
}
782069860236atagen2026-10-10 12:44 UTC153
782069860236atagen2026-10-10 12:44 UTC154
#[tokio::test]
782069860236atagen2026-10-10 12:44 UTC155
async fn the_clinic_does_not_advertise_jelq() {
782069860236atagen2026-10-10 12:44 UTC156
    let (app, _) = seeded(Floor::Clinic);
782069860236atagen2026-10-10 12:44 UTC157
    let options = send(&app, "OPTIONS", "/runs/cafebabe", &[], "").await;
782069860236atagen2026-10-10 12:44 UTC158
    assert!(!header(&options, "allow").contains("JELQ"));
782069860236atagen2026-10-10 12:44 UTC159
    let jelq = send(&app, "JELQ", "/runs/cafebabe?technique=wet", &[], "").await;
782069860236atagen2026-10-10 12:44 UTC160
    assert_eq!(jelq.status(), StatusCode::METHOD_NOT_ALLOWED);
782069860236atagen2026-10-10 12:44 UTC161
    assert!(!header(&jelq, "allow").contains("JELQ"));
782069860236atagen2026-10-10 12:44 UTC162
    let (bedroom, _) = seeded(Floor::Bedroom);
782069860236atagen2026-10-10 12:44 UTC163
    let options = send(&bedroom, "OPTIONS", "/runs/cafebabe", &[], "").await;
782069860236atagen2026-10-10 12:44 UTC164
    assert!(header(&options, "allow").contains("JELQ"));
a9b016e4b248atagen2026-10-10 08:12 UTC165
}
a9b016e4b248atagen2026-10-10 08:12 UTC166
a9b016e4b248atagen2026-10-10 08:12 UTC167
#[tokio::test]
a9b016e4b248atagen2026-10-10 08:12 UTC168
async fn patch_only_accepts_the_truth() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC169
    let (app, _) = seeded(Floor::Bedroom);
a9b016e4b248atagen2026-10-10 08:12 UTC170
    let patch = [("content-type", "application/json-patch+json")];
a9b016e4b248atagen2026-10-10 08:12 UTC171
    let replace = send(
a9b016e4b248atagen2026-10-10 08:12 UTC172
        &app,
a9b016e4b248atagen2026-10-10 08:12 UTC173
        "PATCH",
a9b016e4b248atagen2026-10-10 08:12 UTC174
        "/runs/cafebabe",
a9b016e4b248atagen2026-10-10 08:12 UTC175
        &patch,
a9b016e4b248atagen2026-10-10 08:12 UTC176
        r#"[{"op":"replace","path":"/length","value":12}]"#,
a9b016e4b248atagen2026-10-10 08:12 UTC177
    )
a9b016e4b248atagen2026-10-10 08:12 UTC178
    .await;
a9b016e4b248atagen2026-10-10 08:12 UTC179
    assert_eq!(replace.status(), StatusCode::UNPROCESSABLE_ENTITY);
a9b016e4b248atagen2026-10-10 08:12 UTC180
    let lie = send(
a9b016e4b248atagen2026-10-10 08:12 UTC181
        &app,
a9b016e4b248atagen2026-10-10 08:12 UTC182
        "PATCH",
a9b016e4b248atagen2026-10-10 08:12 UTC183
        "/runs/cafebabe",
a9b016e4b248atagen2026-10-10 08:12 UTC184
        &patch,
a9b016e4b248atagen2026-10-10 08:12 UTC185
        r#"[{"op":"test","path":"/length","value":40}]"#,
a9b016e4b248atagen2026-10-10 08:12 UTC186
    )
a9b016e4b248atagen2026-10-10 08:12 UTC187
    .await;
a9b016e4b248atagen2026-10-10 08:12 UTC188
    assert_eq!(
a9b016e4b248atagen2026-10-10 08:12 UTC189
        json(lie).await["type"],
a9b016e4b248atagen2026-10-10 08:12 UTC190
        "https://jelq.dev/problems/exaggeration"
a9b016e4b248atagen2026-10-10 08:12 UTC191
    );
a9b016e4b248atagen2026-10-10 08:12 UTC192
}
a9b016e4b248atagen2026-10-10 08:12 UTC193
a9b016e4b248atagen2026-10-10 08:12 UTC194
#[tokio::test]
a9b016e4b248atagen2026-10-10 08:12 UTC195
async fn deleting_the_foreskin_requires_certainty() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC196
    let (app, _) = seeded(Floor::Bedroom);
a9b016e4b248atagen2026-10-10 08:12 UTC197
    let response = send(&app, "DELETE", "/runs/cafebabe/foreskin", &[], "").await;
a9b016e4b248atagen2026-10-10 08:12 UTC198
    assert_eq!(response.status(), StatusCode::PRECONDITION_REQUIRED);
782069860236atagen2026-10-10 12:44 UTC199
    let look = send(&app, "GET", "/runs/cafebabe/foreskin", &[], "").await;
ec6fd4bb5b60atagen2026-10-11 04:34 UTC200
    assert_eq!(look.status(), StatusCode::OK);
782069860236atagen2026-10-10 12:44 UTC201
    let etag = header(&look, "etag").to_owned();
ec6fd4bb5b60atagen2026-10-11 04:34 UTC202
    assert!(text(look).await.contains('~'));
a9b016e4b248atagen2026-10-10 08:12 UTC203
    let response = send(
a9b016e4b248atagen2026-10-10 08:12 UTC204
        &app,
a9b016e4b248atagen2026-10-10 08:12 UTC205
        "DELETE",
a9b016e4b248atagen2026-10-10 08:12 UTC206
        "/runs/cafebabe/foreskin",
a9b016e4b248atagen2026-10-10 08:12 UTC207
        &[("if-match", &etag)],
a9b016e4b248atagen2026-10-10 08:12 UTC208
        "",
a9b016e4b248atagen2026-10-10 08:12 UTC209
    )
a9b016e4b248atagen2026-10-10 08:12 UTC210
    .await;
a9b016e4b248atagen2026-10-10 08:12 UTC211
    assert_eq!(response.status(), StatusCode::NO_CONTENT);
a9b016e4b248atagen2026-10-10 08:12 UTC212
    let response = send(&app, "GET", "/runs/cafebabe/foreskin", &[], "").await;
a9b016e4b248atagen2026-10-10 08:12 UTC213
    assert_eq!(response.status(), StatusCode::GONE);
a9b016e4b248atagen2026-10-10 08:12 UTC214
}
a9b016e4b248atagen2026-10-10 08:12 UTC215
a9b016e4b248atagen2026-10-10 08:12 UTC216
#[tokio::test]
a9b016e4b248atagen2026-10-10 08:12 UTC217
async fn length_is_chunked_until_the_ruler() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC218
    let (app, _) = seeded(Floor::Bedroom);
a9b016e4b248atagen2026-10-10 08:12 UTC219
    let lie = send(&app, "GET", "/runs/cafebabe/length", &[], "").await;
a9b016e4b248atagen2026-10-10 08:12 UTC220
    assert!(!lie.headers().contains_key(header::CONTENT_LENGTH));
ad00e0e051a1atagen2026-10-10 14:00 UTC221
    let app = seeded_with(Floor::Bedroom, |run| run.items.ruler = true);
a9b016e4b248atagen2026-10-10 08:12 UTC222
    let truth = send(&app, "GET", "/runs/cafebabe/length", &[], "").await;
782069860236atagen2026-10-10 12:44 UTC223
    let declared: usize = header(&truth, "content-length").parse().unwrap();
782069860236atagen2026-10-10 12:44 UTC224
    let body = text(truth).await;
a9b016e4b248atagen2026-10-10 08:12 UTC225
    assert_eq!(body.len(), declared);
782069860236atagen2026-10-10 12:44 UTC226
    assert!(body.starts_with("8=") && body.ends_with('D'));
782069860236atagen2026-10-10 12:44 UTC227
}
782069860236atagen2026-10-10 12:44 UTC228
782069860236atagen2026-10-10 12:44 UTC229
#[tokio::test]
782069860236atagen2026-10-10 12:44 UTC230
async fn length_supports_inch_ranges() {
782069860236atagen2026-10-10 12:44 UTC231
    let (app, _) = seeded(Floor::Bedroom);
782069860236atagen2026-10-10 12:44 UTC232
    let response = send(
782069860236atagen2026-10-10 12:44 UTC233
        &app,
782069860236atagen2026-10-10 12:44 UTC234
        "GET",
782069860236atagen2026-10-10 12:44 UTC235
        "/runs/cafebabe/length",
782069860236atagen2026-10-10 12:44 UTC236
        &[("range", "inches=0-1")],
782069860236atagen2026-10-10 12:44 UTC237
        "",
782069860236atagen2026-10-10 12:44 UTC238
    )
782069860236atagen2026-10-10 12:44 UTC239
    .await;
782069860236atagen2026-10-10 12:44 UTC240
    assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT);
782069860236atagen2026-10-10 12:44 UTC241
    assert!(header(&response, "content-range").starts_with("inches 0.00-1.00/"));
782069860236atagen2026-10-10 12:44 UTC242
    assert_eq!(text(response).await.len(), 25);
782069860236atagen2026-10-10 12:44 UTC243
    let absurd = send(
782069860236atagen2026-10-10 12:44 UTC244
        &app,
782069860236atagen2026-10-10 12:44 UTC245
        "GET",
782069860236atagen2026-10-10 12:44 UTC246
        "/runs/cafebabe/length",
782069860236atagen2026-10-10 12:44 UTC247
        &[("range", "inches=40-50")],
782069860236atagen2026-10-10 12:44 UTC248
        "",
782069860236atagen2026-10-10 12:44 UTC249
    )
782069860236atagen2026-10-10 12:44 UTC250
    .await;
782069860236atagen2026-10-10 12:44 UTC251
    assert_eq!(absurd.status(), StatusCode::RANGE_NOT_SATISFIABLE);
d46f9d0b953aatagen2026-10-10 14:41 UTC252
    let malformed = send(
d46f9d0b953aatagen2026-10-10 14:41 UTC253
        &app,
d46f9d0b953aatagen2026-10-10 14:41 UTC254
        "GET",
d46f9d0b953aatagen2026-10-10 14:41 UTC255
        "/runs/cafebabe/length",
d46f9d0b953aatagen2026-10-10 14:41 UTC256
        &[("range", "inches=6.17")],
d46f9d0b953aatagen2026-10-10 14:41 UTC257
        "",
d46f9d0b953aatagen2026-10-10 14:41 UTC258
    )
d46f9d0b953aatagen2026-10-10 14:41 UTC259
    .await;
d46f9d0b953aatagen2026-10-10 14:41 UTC260
    assert_eq!(malformed.status(), StatusCode::OK);
d46f9d0b953aatagen2026-10-10 14:41 UTC261
    let suffix = send(
d46f9d0b953aatagen2026-10-10 14:41 UTC262
        &app,
d46f9d0b953aatagen2026-10-10 14:41 UTC263
        "GET",
d46f9d0b953aatagen2026-10-10 14:41 UTC264
        "/runs/cafebabe/length",
d46f9d0b953aatagen2026-10-10 14:41 UTC265
        &[("range", "inches=-1")],
d46f9d0b953aatagen2026-10-10 14:41 UTC266
        "",
d46f9d0b953aatagen2026-10-10 14:41 UTC267
    )
d46f9d0b953aatagen2026-10-10 14:41 UTC268
    .await;
d46f9d0b953aatagen2026-10-10 14:41 UTC269
    assert_eq!(suffix.status(), StatusCode::PARTIAL_CONTENT);
d46f9d0b953aatagen2026-10-10 14:41 UTC270
    assert!(text(suffix).await.ends_with('D'));
d46f9d0b953aatagen2026-10-10 14:41 UTC271
    for (range, status) in [
d46f9d0b953aatagen2026-10-10 14:41 UTC272
        ("inches=1-", StatusCode::PARTIAL_CONTENT),
d46f9d0b953aatagen2026-10-10 14:41 UTC273
        ("inches=40-", StatusCode::RANGE_NOT_SATISFIABLE),
d46f9d0b953aatagen2026-10-10 14:41 UTC274
        ("inches=-0", StatusCode::RANGE_NOT_SATISFIABLE),
d46f9d0b953aatagen2026-10-10 14:41 UTC275
        ("inches=2-1", StatusCode::OK),
d46f9d0b953aatagen2026-10-10 14:41 UTC276
        ("inches=NaN-2", StatusCode::OK),
d46f9d0b953aatagen2026-10-10 14:41 UTC277
        ("inches=0-inf", StatusCode::OK),
d46f9d0b953aatagen2026-10-10 14:41 UTC278
    ] {
d46f9d0b953aatagen2026-10-10 14:41 UTC279
        let response = send(
d46f9d0b953aatagen2026-10-10 14:41 UTC280
            &app,
d46f9d0b953aatagen2026-10-10 14:41 UTC281
            "GET",
d46f9d0b953aatagen2026-10-10 14:41 UTC282
            "/runs/cafebabe/length",
d46f9d0b953aatagen2026-10-10 14:41 UTC283
            &[("range", range)],
d46f9d0b953aatagen2026-10-10 14:41 UTC284
            "",
d46f9d0b953aatagen2026-10-10 14:41 UTC285
        )
d46f9d0b953aatagen2026-10-10 14:41 UTC286
        .await;
d46f9d0b953aatagen2026-10-10 14:41 UTC287
        assert_eq!(response.status(), status, "{range}");
d46f9d0b953aatagen2026-10-10 14:41 UTC288
    }
a9b016e4b248atagen2026-10-10 08:12 UTC289
}
a9b016e4b248atagen2026-10-10 08:12 UTC290
a9b016e4b248atagen2026-10-10 08:12 UTC291
#[tokio::test]
a9b016e4b248atagen2026-10-10 08:12 UTC292
async fn the_clinic_escalates_toward_honesty() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC293
    let (app, _) = seeded(Floor::Clinic);
a9b016e4b248atagen2026-10-10 08:12 UTC294
    let mut hints = Vec::new();
a9b016e4b248atagen2026-10-10 08:12 UTC295
    for _ in 0..3 {
a9b016e4b248atagen2026-10-10 08:12 UTC296
        let response = send(
a9b016e4b248atagen2026-10-10 08:12 UTC297
            &app,
a9b016e4b248atagen2026-10-10 08:12 UTC298
            "GET",
a9b016e4b248atagen2026-10-10 08:12 UTC299
            "/runs/cafebabe/diagnosis",
a9b016e4b248atagen2026-10-10 08:12 UTC300
            &[("authorization", "Excuse \"changing\"")],
a9b016e4b248atagen2026-10-10 08:12 UTC301
            "",
a9b016e4b248atagen2026-10-10 08:12 UTC302
        )
a9b016e4b248atagen2026-10-10 08:12 UTC303
        .await;
a9b016e4b248atagen2026-10-10 08:12 UTC304
        assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
782069860236atagen2026-10-10 12:44 UTC305
        hints.push(header(&response, "www-authenticate").to_owned());
a9b016e4b248atagen2026-10-10 08:12 UTC306
    }
a9b016e4b248atagen2026-10-10 08:12 UTC307
    assert!(hints[2].contains("Be honest."));
a9b016e4b248atagen2026-10-10 08:12 UTC308
}
a9b016e4b248atagen2026-10-10 08:12 UTC309
a9b016e4b248atagen2026-10-10 08:12 UTC310
#[tokio::test]
a9b016e4b248atagen2026-10-10 08:12 UTC311
async fn the_true_ending_is_not_modified() {
782069860236atagen2026-10-10 12:44 UTC312
    let (early, _) = seeded(Floor::Bedroom);
a9b016e4b248atagen2026-10-10 08:12 UTC313
    let response = send(&early, "GET", "/runs/cafebabe/diagnosis", &[], "").await;
a9b016e4b248atagen2026-10-10 08:12 UTC314
    assert_eq!(response.status(), StatusCode::TOO_EARLY);
a9b016e4b248atagen2026-10-10 08:12 UTC315
782069860236atagen2026-10-10 12:44 UTC316
    let (app, etag) = seeded(Floor::Clinic);
782069860236atagen2026-10-10 12:44 UTC317
    assert!(
782069860236atagen2026-10-10 12:44 UTC318
        !etag.contains("cafebabe"),
782069860236atagen2026-10-10 12:44 UTC319
        "the origin ETag must not be derivable from the id"
782069860236atagen2026-10-10 12:44 UTC320
    );
a9b016e4b248atagen2026-10-10 08:12 UTC321
    let response = send(
a9b016e4b248atagen2026-10-10 08:12 UTC322
        &app,
a9b016e4b248atagen2026-10-10 08:12 UTC323
        "GET",
a9b016e4b248atagen2026-10-10 08:12 UTC324
        "/runs/cafebabe/diagnosis",
a9b016e4b248atagen2026-10-10 08:12 UTC325
        &[
a9b016e4b248atagen2026-10-10 08:12 UTC326
            ("authorization", "Bearer honesty"),
a9b016e4b248atagen2026-10-10 08:12 UTC327
            ("if-none-match", &etag),
a9b016e4b248atagen2026-10-10 08:12 UTC328
        ],
a9b016e4b248atagen2026-10-10 08:12 UTC329
        "",
a9b016e4b248atagen2026-10-10 08:12 UTC330
    )
a9b016e4b248atagen2026-10-10 08:12 UTC331
    .await;
a9b016e4b248atagen2026-10-10 08:12 UTC332
    assert_eq!(response.status(), StatusCode::NOT_MODIFIED);
782069860236atagen2026-10-10 12:44 UTC333
    assert_eq!(header(&response, "x-diagnosis"), "normal");
a9b016e4b248atagen2026-10-10 08:12 UTC334
}
a9b016e4b248atagen2026-10-10 08:12 UTC335
a9b016e4b248atagen2026-10-10 08:12 UTC336
#[tokio::test]
9683736a2c5eatagen2026-10-10 13:15 UTC337
async fn mum_accepts_a_known_excuse() {
9683736a2c5eatagen2026-10-10 13:15 UTC338
    let app = seeded_with(Floor::Bedroom, mum_at_the_door);
a9b016e4b248atagen2026-10-10 08:12 UTC339
    let response = send(&app, "GET", "/runs/cafebabe", &[], "").await;
a9b016e4b248atagen2026-10-10 08:12 UTC340
    assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
9683736a2c5eatagen2026-10-10 13:15 UTC341
    assert_eq!(header(&response, "via"), "1.1 mum");
a9b016e4b248atagen2026-10-10 08:12 UTC342
    let response = send(
a9b016e4b248atagen2026-10-10 08:12 UTC343
        &app,
a9b016e4b248atagen2026-10-10 08:12 UTC344
        "GET",
a9b016e4b248atagen2026-10-10 08:12 UTC345
        "/runs/cafebabe",
a9b016e4b248atagen2026-10-10 08:12 UTC346
        &[("authorization", "Excuse \"homework\"")],
a9b016e4b248atagen2026-10-10 08:12 UTC347
        "",
a9b016e4b248atagen2026-10-10 08:12 UTC348
    )
a9b016e4b248atagen2026-10-10 08:12 UTC349
    .await;
a9b016e4b248atagen2026-10-10 08:12 UTC350
    assert_eq!(response.status(), StatusCode::OK);
a9b016e4b248atagen2026-10-10 08:12 UTC351
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC352
782069860236atagen2026-10-10 12:44 UTC353
#[tokio::test]
9683736a2c5eatagen2026-10-10 13:15 UTC354
async fn mum_runs_out_of_patience() {
9683736a2c5eatagen2026-10-10 13:15 UTC355
    let app = seeded_with(Floor::Bedroom, mum_at_the_door);
782069860236atagen2026-10-10 12:44 UTC356
    let lie = [("authorization", "Excuse \"it is for science\"")];
782069860236atagen2026-10-10 12:44 UTC357
    for _ in 0..2 {
782069860236atagen2026-10-10 12:44 UTC358
        let response = send(&app, "GET", "/runs/cafebabe", &lie, "").await;
782069860236atagen2026-10-10 12:44 UTC359
        assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
782069860236atagen2026-10-10 12:44 UTC360
    }
782069860236atagen2026-10-10 12:44 UTC361
    let caught = send(&app, "GET", "/runs/cafebabe", &lie, "").await;
782069860236atagen2026-10-10 12:44 UTC362
    assert_eq!(caught.status(), StatusCode::GONE);
782069860236atagen2026-10-10 12:44 UTC363
    assert_eq!(
782069860236atagen2026-10-10 12:44 UTC364
        json(caught).await["type"],
782069860236atagen2026-10-10 12:44 UTC365
        "https://jelq.dev/problems/caught"
782069860236atagen2026-10-10 12:44 UTC366
    );
782069860236atagen2026-10-10 12:44 UTC367
}
782069860236atagen2026-10-10 12:44 UTC368
782069860236atagen2026-10-10 12:44 UTC369
#[tokio::test]
9683736a2c5eatagen2026-10-10 13:15 UTC370
async fn mum_remembers_excuses_by_christmas() {
9683736a2c5eatagen2026-10-10 13:15 UTC371
    let app = seeded_with(Floor::Nans, |run| {
9683736a2c5eatagen2026-10-10 13:15 UTC372
        mum_at_the_door(run);
782069860236atagen2026-10-10 12:44 UTC373
        run.heard.push("homework".into());
782069860236atagen2026-10-10 12:44 UTC374
    });
782069860236atagen2026-10-10 12:44 UTC375
    let basic = send(
782069860236atagen2026-10-10 12:44 UTC376
        &app,
782069860236atagen2026-10-10 12:44 UTC377
        "GET",
782069860236atagen2026-10-10 12:44 UTC378
        "/runs/cafebabe",
782069860236atagen2026-10-10 12:44 UTC379
        &[("authorization", "Excuse \"changing\"")],
782069860236atagen2026-10-10 12:44 UTC380
        "",
782069860236atagen2026-10-10 12:44 UTC381
    )
782069860236atagen2026-10-10 12:44 UTC382
    .await;
782069860236atagen2026-10-10 12:44 UTC383
    assert_eq!(basic.status(), StatusCode::UNAUTHORIZED);
782069860236atagen2026-10-10 12:44 UTC384
    assert!(header(&basic, "www-authenticate").starts_with("Digest"));
782069860236atagen2026-10-10 12:44 UTC385
    let replay = send(
782069860236atagen2026-10-10 12:44 UTC386
        &app,
782069860236atagen2026-10-10 12:44 UTC387
        "GET",
782069860236atagen2026-10-10 12:44 UTC388
        "/runs/cafebabe",
782069860236atagen2026-10-10 12:44 UTC389
        &[(
782069860236atagen2026-10-10 12:44 UTC390
            "authorization",
782069860236atagen2026-10-10 12:44 UTC391
            "Digest username=\"homework\", nonce=\"abc\"",
782069860236atagen2026-10-10 12:44 UTC392
        )],
782069860236atagen2026-10-10 12:44 UTC393
        "",
782069860236atagen2026-10-10 12:44 UTC394
    )
782069860236atagen2026-10-10 12:44 UTC395
    .await;
782069860236atagen2026-10-10 12:44 UTC396
    assert_eq!(
782069860236atagen2026-10-10 12:44 UTC397
        json(replay).await["type"],
782069860236atagen2026-10-10 12:44 UTC398
        "https://jelq.dev/problems/excuse-replayed"
782069860236atagen2026-10-10 12:44 UTC399
    );
782069860236atagen2026-10-10 12:44 UTC400
    let fresh = send(
782069860236atagen2026-10-10 12:44 UTC401
        &app,
782069860236atagen2026-10-10 12:44 UTC402
        "GET",
782069860236atagen2026-10-10 12:44 UTC403
        "/runs/cafebabe",
782069860236atagen2026-10-10 12:44 UTC404
        &[(
782069860236atagen2026-10-10 12:44 UTC405
            "authorization",
9683736a2c5eatagen2026-10-10 13:15 UTC406
            "Digest username=\"wrapping presents\", nonce=\"abc\"",
782069860236atagen2026-10-10 12:44 UTC407
        )],
782069860236atagen2026-10-10 12:44 UTC408
        "",
782069860236atagen2026-10-10 12:44 UTC409
    )
782069860236atagen2026-10-10 12:44 UTC410
    .await;
782069860236atagen2026-10-10 12:44 UTC411
    assert_eq!(fresh.status(), StatusCode::OK);
782069860236atagen2026-10-10 12:44 UTC412
}
782069860236atagen2026-10-10 12:44 UTC413
782069860236atagen2026-10-10 12:44 UTC414
#[tokio::test]
782069860236atagen2026-10-10 12:44 UTC415
async fn a_pulled_muscle_is_a_true_excuse() {
9683736a2c5eatagen2026-10-10 13:15 UTC416
    let app = seeded_with(Floor::Nans, |run| {
9683736a2c5eatagen2026-10-10 13:15 UTC417
        mum_at_the_door(run);
782069860236atagen2026-10-10 12:44 UTC418
        run.conditions.pulled_muscle = true;
782069860236atagen2026-10-10 12:44 UTC419
        run.heard.push("pulled a muscle".into());
782069860236atagen2026-10-10 12:44 UTC420
    });
782069860236atagen2026-10-10 12:44 UTC421
    let response = send(
782069860236atagen2026-10-10 12:44 UTC422
        &app,
782069860236atagen2026-10-10 12:44 UTC423
        "GET",
782069860236atagen2026-10-10 12:44 UTC424
        "/runs/cafebabe",
782069860236atagen2026-10-10 12:44 UTC425
        &[(
782069860236atagen2026-10-10 12:44 UTC426
            "authorization",
782069860236atagen2026-10-10 12:44 UTC427
            "Digest username=\"pulled a muscle\", nonce=\"abc\"",
782069860236atagen2026-10-10 12:44 UTC428
        )],
782069860236atagen2026-10-10 12:44 UTC429
        "",
782069860236atagen2026-10-10 12:44 UTC430
    )
782069860236atagen2026-10-10 12:44 UTC431
    .await;
782069860236atagen2026-10-10 12:44 UTC432
    assert_eq!(response.status(), StatusCode::OK);
782069860236atagen2026-10-10 12:44 UTC433
    assert!(header(&response, "x-stamina").ends_with("/7"));
dfa38cf4cd4batagen2026-10-10 09:17 UTC434
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC435
dfa38cf4cd4batagen2026-10-10 09:17 UTC436
#[tokio::test]
dfa38cf4cd4batagen2026-10-10 09:17 UTC437
async fn floors_end_when_they_end() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC438
    let app = seeded_with(Floor::Bedroom, |_| {});
dfa38cf4cd4batagen2026-10-10 09:17 UTC439
    let early = send(&app, "POST", "/runs/cafebabe/floor", &[], "").await;
dfa38cf4cd4batagen2026-10-10 09:17 UTC440
    assert_eq!(early.status(), StatusCode::TOO_EARLY);
782069860236atagen2026-10-10 12:44 UTC441
    let app = seeded_with(Floor::Bedroom, |run| run.here.turns = 6);
dfa38cf4cd4batagen2026-10-10 09:17 UTC442
    let next = send(&app, "POST", "/runs/cafebabe/floor", &[], "").await;
782069860236atagen2026-10-10 12:44 UTC443
    assert!(
782069860236atagen2026-10-10 12:44 UTC444
        next.status() == StatusCode::SEE_OTHER || next.status() == StatusCode::CONFLICT,
9683736a2c5eatagen2026-10-10 13:15 UTC445
        "either the floor changed or the zip did"
782069860236atagen2026-10-10 12:44 UTC446
    );
dfa38cf4cd4batagen2026-10-10 09:17 UTC447
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC448
dfa38cf4cd4batagen2026-10-10 09:17 UTC449
#[tokio::test]
dfa38cf4cd4batagen2026-10-10 09:17 UTC450
async fn pump_bro_upsells() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC451
    let closed = seeded_with(Floor::Bedroom, |_| {});
dfa38cf4cd4batagen2026-10-10 09:17 UTC452
    let response = send(&closed, "GET", "/runs/cafebabe/shop", &[], "").await;
dfa38cf4cd4batagen2026-10-10 09:17 UTC453
    assert_eq!(response.status(), StatusCode::NOT_FOUND);
dfa38cf4cd4batagen2026-10-10 09:17 UTC454
9683736a2c5eatagen2026-10-10 13:15 UTC455
    let app = seeded_with(Floor::ChangingRoom, |_| {});
dfa38cf4cd4batagen2026-10-10 09:17 UTC456
    let pump = send(&app, "POST", "/runs/cafebabe/shop/pump", &[], "").await;
dfa38cf4cd4batagen2026-10-10 09:17 UTC457
    assert_eq!(pump.status(), StatusCode::UPGRADE_REQUIRED);
782069860236atagen2026-10-10 12:44 UTC458
    assert_eq!(header(&pump, "upgrade"), "pump-pro/2.0");
dfa38cf4cd4batagen2026-10-10 09:17 UTC459
    let pro = send(&app, "POST", "/runs/cafebabe/shop/pump-pro", &[], "").await;
dfa38cf4cd4batagen2026-10-10 09:17 UTC460
    assert_eq!(pro.status(), StatusCode::CREATED);
dfa38cf4cd4batagen2026-10-10 09:17 UTC461
    let broke = send(&app, "POST", "/runs/cafebabe/shop/extender", &[], "").await;
dfa38cf4cd4batagen2026-10-10 09:17 UTC462
    assert_eq!(broke.status(), StatusCode::PAYMENT_REQUIRED);
dfa38cf4cd4batagen2026-10-10 09:17 UTC463
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC464
dfa38cf4cd4batagen2026-10-10 09:17 UTC465
#[tokio::test]
782069860236atagen2026-10-10 12:44 UTC466
async fn only_the_key_opens_the_cage() {
9683736a2c5eatagen2026-10-10 13:15 UTC467
    let app = seeded_with(Floor::ChangingRoom, |run| run.items.cage = true);
782069860236atagen2026-10-10 12:44 UTC468
    send(&app, "POST", "/runs/cafebabe/shop/ice-pack", &[], "").await;
782069860236atagen2026-10-10 12:44 UTC469
    let still = send(
782069860236atagen2026-10-10 12:44 UTC470
        &app,
782069860236atagen2026-10-10 12:44 UTC471
        "GET",
782069860236atagen2026-10-10 12:44 UTC472
        "/runs/cafebabe/inventory/chastity-cage",
782069860236atagen2026-10-10 12:44 UTC473
        &[],
782069860236atagen2026-10-10 12:44 UTC474
        "",
782069860236atagen2026-10-10 12:44 UTC475
    )
782069860236atagen2026-10-10 12:44 UTC476
    .await;
782069860236atagen2026-10-10 12:44 UTC477
    assert_eq!(still.status(), StatusCode::OK);
782069860236atagen2026-10-10 12:44 UTC478
    send(&app, "POST", "/runs/cafebabe/shop/cage-key", &[], "").await;
782069860236atagen2026-10-10 12:44 UTC479
    let gone = send(
782069860236atagen2026-10-10 12:44 UTC480
        &app,
782069860236atagen2026-10-10 12:44 UTC481
        "GET",
782069860236atagen2026-10-10 12:44 UTC482
        "/runs/cafebabe/inventory/chastity-cage",
782069860236atagen2026-10-10 12:44 UTC483
        &[],
782069860236atagen2026-10-10 12:44 UTC484
        "",
782069860236atagen2026-10-10 12:44 UTC485
    )
782069860236atagen2026-10-10 12:44 UTC486
    .await;
782069860236atagen2026-10-10 12:44 UTC487
    assert_eq!(gone.status(), StatusCode::NOT_FOUND);
782069860236atagen2026-10-10 12:44 UTC488
}
782069860236atagen2026-10-10 12:44 UTC489
782069860236atagen2026-10-10 12:44 UTC490
#[tokio::test]
dfa38cf4cd4batagen2026-10-10 09:17 UTC491
async fn combos_are_multi_status_xml() {
782069860236atagen2026-10-10 12:44 UTC492
    let app = seeded_with(Floor::Bathroom, |run| {
782069860236atagen2026-10-10 12:44 UTC493
        run.foreskin = Some(1.0);
782069860236atagen2026-10-10 12:44 UTC494
        run.items.blinds = true;
782069860236atagen2026-10-10 12:44 UTC495
    });
dfa38cf4cd4batagen2026-10-10 09:17 UTC496
    send(&app, "JELQ", "/runs/cafebabe?technique=warmup", &[], "").await;
dfa38cf4cd4batagen2026-10-10 09:17 UTC497
    let response = send(
dfa38cf4cd4batagen2026-10-10 09:17 UTC498
        &app,
dfa38cf4cd4batagen2026-10-10 09:17 UTC499
        "JELQ",
dfa38cf4cd4batagen2026-10-10 09:17 UTC500
        "/runs/cafebabe?technique=wet,stretch",
dfa38cf4cd4batagen2026-10-10 09:17 UTC501
        &[("content-type", "application/baby-oil")],
dfa38cf4cd4batagen2026-10-10 09:17 UTC502
        "",
dfa38cf4cd4batagen2026-10-10 09:17 UTC503
    )
dfa38cf4cd4batagen2026-10-10 09:17 UTC504
    .await;
dfa38cf4cd4batagen2026-10-10 09:17 UTC505
    assert_eq!(response.status(), StatusCode::MULTI_STATUS);
782069860236atagen2026-10-10 12:44 UTC506
    assert!(text(response).await.contains("<D:multistatus"));
dfa38cf4cd4batagen2026-10-10 09:17 UTC507
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC508
dfa38cf4cd4batagen2026-10-10 09:17 UTC509
#[tokio::test]
dfa38cf4cd4batagen2026-10-10 09:17 UTC510
async fn the_elder_is_stale() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC511
    let app = seeded_with(Floor::Forum, |run| run.length = 5.0);
dfa38cf4cd4batagen2026-10-10 09:17 UTC512
    let unverified = send(&app, "GET", "/runs/cafebabe/elder", &[], "").await;
dfa38cf4cd4batagen2026-10-10 09:17 UTC513
    assert_eq!(
dfa38cf4cd4batagen2026-10-10 09:17 UTC514
        unverified.status(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC515
        StatusCode::PROXY_AUTHENTICATION_REQUIRED
dfa38cf4cd4batagen2026-10-10 09:17 UTC516
    );
dfa38cf4cd4batagen2026-10-10 09:17 UTC517
    let lie = send(
dfa38cf4cd4batagen2026-10-10 09:17 UTC518
        &app,
dfa38cf4cd4batagen2026-10-10 09:17 UTC519
        "GET",
dfa38cf4cd4batagen2026-10-10 09:17 UTC520
        "/runs/cafebabe/elder",
dfa38cf4cd4batagen2026-10-10 09:17 UTC521
        &[("proxy-authorization", "Ruler 9")],
dfa38cf4cd4batagen2026-10-10 09:17 UTC522
        "",
dfa38cf4cd4batagen2026-10-10 09:17 UTC523
    )
dfa38cf4cd4batagen2026-10-10 09:17 UTC524
    .await;
dfa38cf4cd4batagen2026-10-10 09:17 UTC525
    assert_eq!(lie.status(), StatusCode::BAD_REQUEST);
dfa38cf4cd4batagen2026-10-10 09:17 UTC526
    let csv = send(
dfa38cf4cd4batagen2026-10-10 09:17 UTC527
        &app,
dfa38cf4cd4batagen2026-10-10 09:17 UTC528
        "GET",
dfa38cf4cd4batagen2026-10-10 09:17 UTC529
        "/runs/cafebabe/elder",
dfa38cf4cd4batagen2026-10-10 09:17 UTC530
        &[("proxy-authorization", "Ruler 5.0")],
dfa38cf4cd4batagen2026-10-10 09:17 UTC531
        "",
dfa38cf4cd4batagen2026-10-10 09:17 UTC532
    )
dfa38cf4cd4batagen2026-10-10 09:17 UTC533
    .await;
dfa38cf4cd4batagen2026-10-10 09:17 UTC534
    assert_eq!(csv.status(), StatusCode::OK);
dfa38cf4cd4batagen2026-10-10 09:17 UTC535
    let stale = send(
dfa38cf4cd4batagen2026-10-10 09:17 UTC536
        &app,
dfa38cf4cd4batagen2026-10-10 09:17 UTC537
        "GET",
dfa38cf4cd4batagen2026-10-10 09:17 UTC538
        "/runs/cafebabe/elder",
dfa38cf4cd4batagen2026-10-10 09:17 UTC539
        &[("if-modified-since", "Mon, 15 Jun 2009 00:00:00 GMT")],
dfa38cf4cd4batagen2026-10-10 09:17 UTC540
        "",
dfa38cf4cd4batagen2026-10-10 09:17 UTC541
    )
dfa38cf4cd4batagen2026-10-10 09:17 UTC542
    .await;
dfa38cf4cd4batagen2026-10-10 09:17 UTC543
    assert_eq!(stale.status(), StatusCode::NOT_MODIFIED);
782069860236atagen2026-10-10 12:44 UTC544
}
782069860236atagen2026-10-10 12:44 UTC545
782069860236atagen2026-10-10 12:44 UTC546
#[tokio::test]
782069860236atagen2026-10-10 12:44 UTC547
async fn the_displayed_length_is_accepted_as_honest() {
782069860236atagen2026-10-10 12:44 UTC548
    let app = seeded_with(Floor::Forum, |run| run.length = 5.2496);
782069860236atagen2026-10-10 12:44 UTC549
    let response = send(
782069860236atagen2026-10-10 12:44 UTC550
        &app,
782069860236atagen2026-10-10 12:44 UTC551
        "GET",
782069860236atagen2026-10-10 12:44 UTC552
        "/runs/cafebabe/elder",
782069860236atagen2026-10-10 12:44 UTC553
        &[("proxy-authorization", "Ruler 5.25")],
782069860236atagen2026-10-10 12:44 UTC554
        "",
782069860236atagen2026-10-10 12:44 UTC555
    )
782069860236atagen2026-10-10 12:44 UTC556
    .await;
782069860236atagen2026-10-10 12:44 UTC557
    assert_eq!(response.status(), StatusCode::OK);
dfa38cf4cd4batagen2026-10-10 09:17 UTC558
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC559
dfa38cf4cd4batagen2026-10-10 09:17 UTC560
#[tokio::test]
dfa38cf4cd4batagen2026-10-10 09:17 UTC561
async fn the_hotel_has_a_captive_portal() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC562
    let app = seeded_with(Floor::Hotel, |_| {});
dfa38cf4cd4batagen2026-10-10 09:17 UTC563
    let blocked = send(&app, "GET", "/runs/cafebabe", &[], "").await;
dfa38cf4cd4batagen2026-10-10 09:17 UTC564
    assert_eq!(
dfa38cf4cd4batagen2026-10-10 09:17 UTC565
        blocked.status(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC566
        StatusCode::NETWORK_AUTHENTICATION_REQUIRED
dfa38cf4cd4batagen2026-10-10 09:17 UTC567
    );
dfa38cf4cd4batagen2026-10-10 09:17 UTC568
    let accepted = send(&app, "POST", "/runs/cafebabe/portal", &[], "").await;
dfa38cf4cd4batagen2026-10-10 09:17 UTC569
    assert_eq!(accepted.status(), StatusCode::SEE_OTHER);
dfa38cf4cd4batagen2026-10-10 09:17 UTC570
    let online = send(&app, "GET", "/runs/cafebabe", &[], "").await;
dfa38cf4cd4batagen2026-10-10 09:17 UTC571
    assert_eq!(online.status(), StatusCode::OK);
dfa38cf4cd4batagen2026-10-10 09:17 UTC572
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC573
dfa38cf4cd4batagen2026-10-10 09:17 UTC574
#[tokio::test]
782069860236atagen2026-10-10 12:44 UTC575
async fn the_internet_never_forgets() {
782069860236atagen2026-10-10 12:44 UTC576
    let app = seeded_with(Floor::Bedroom, |run| {
782069860236atagen2026-10-10 12:44 UTC577
        run.filmed_at = Some(std::time::SystemTime::now());
dfa38cf4cd4batagen2026-10-10 09:17 UTC578
    });
782069860236atagen2026-10-10 12:44 UTC579
    let footage = send(&app, "GET", "/runs/cafebabe/footage", &[], "").await;
782069860236atagen2026-10-10 12:44 UTC580
    assert_eq!(footage.status(), StatusCode::OK);
99524cf9f17catagen2026-10-10 10:53 UTC581
    assert_eq!(
782069860236atagen2026-10-10 12:44 UTC582
        header(&footage, "cache-control"),
782069860236atagen2026-10-10 12:44 UTC583
        "public, max-age=31536000, immutable"
99524cf9f17catagen2026-10-10 10:53 UTC584
    );
782069860236atagen2026-10-10 12:44 UTC585
    assert!(footage.headers().contains_key(header::AGE));
782069860236atagen2026-10-10 12:44 UTC586
    let delete = send(&app, "DELETE", "/runs/cafebabe/footage", &[], "").await;
782069860236atagen2026-10-10 12:44 UTC587
    assert_eq!(delete.status(), StatusCode::METHOD_NOT_ALLOWED);
782069860236atagen2026-10-10 12:44 UTC588
    assert!(!header(&delete, "allow").contains("DELETE"));
99524cf9f17catagen2026-10-10 10:53 UTC589
}
99524cf9f17catagen2026-10-10 10:53 UTC590
99524cf9f17catagen2026-10-10 10:53 UTC591
#[tokio::test]
99524cf9f17catagen2026-10-10 10:53 UTC592
async fn forged_ruler_cookies_are_lies() {
99524cf9f17catagen2026-10-10 10:53 UTC593
    let forged = send(
782069860236atagen2026-10-10 12:44 UTC594
        &app(),
99524cf9f17catagen2026-10-10 10:53 UTC595
        "GET",
99524cf9f17catagen2026-10-10 10:53 UTC596
        "/leaderboard?sort=content-length",
99524cf9f17catagen2026-10-10 10:53 UTC597
        &[("cookie", "ruler=1")],
99524cf9f17catagen2026-10-10 10:53 UTC598
        "",
99524cf9f17catagen2026-10-10 10:53 UTC599
    )
99524cf9f17catagen2026-10-10 10:53 UTC600
    .await;
99524cf9f17catagen2026-10-10 10:53 UTC601
    assert_eq!(forged.status(), StatusCode::BAD_REQUEST);
99524cf9f17catagen2026-10-10 10:53 UTC602
}
99524cf9f17catagen2026-10-10 10:53 UTC603
99524cf9f17catagen2026-10-10 10:53 UTC604
#[tokio::test]
782069860236atagen2026-10-10 12:44 UTC605
async fn listings_never_reveal_the_private_id() {
782069860236atagen2026-10-10 12:44 UTC606
    let app = app();
782069860236atagen2026-10-10 12:44 UTC607
    let created = send(&app, "POST", "/runs", &[], "").await;
782069860236atagen2026-10-10 12:44 UTC608
    let location = header(&created, "location").to_owned();
782069860236atagen2026-10-10 12:44 UTC609
    let board = text(send(&app, "GET", "/leaderboard", &[], "").await).await;
782069860236atagen2026-10-10 12:44 UTC610
    let private = location.trim_start_matches("/runs/");
782069860236atagen2026-10-10 12:44 UTC611
    assert!(!board.contains(private));
782069860236atagen2026-10-10 12:44 UTC612
    let entry: Value = serde_json::from_str(&board).unwrap();
782069860236atagen2026-10-10 12:44 UTC613
    let public = entry["entries"][0]["public_id"].as_str().unwrap();
782069860236atagen2026-10-10 12:44 UTC614
    let guess = send(&app, "DELETE", &format!("/runs/{public}"), &[], "").await;
782069860236atagen2026-10-10 12:44 UTC615
    assert_eq!(guess.status(), StatusCode::NOT_FOUND);
782069860236atagen2026-10-10 12:44 UTC616
}
782069860236atagen2026-10-10 12:44 UTC617
782069860236atagen2026-10-10 12:44 UTC618
#[tokio::test]
99524cf9f17catagen2026-10-10 10:53 UTC619
async fn seeded_runs_are_unranked() {
782069860236atagen2026-10-10 12:44 UTC620
    let app = app();
99524cf9f17catagen2026-10-10 10:53 UTC621
    send(&app, "POST", "/runs?seed=7", &[], "").await;
99524cf9f17catagen2026-10-10 10:53 UTC622
    let board = json(send(&app, "GET", "/leaderboard", &[], "").await).await;
99524cf9f17catagen2026-10-10 10:53 UTC623
    assert_eq!(board["entries"].as_array().unwrap().len(), 0);
99524cf9f17catagen2026-10-10 10:53 UTC624
    let all = json(send(&app, "GET", "/leaderboard?seeded=include", &[], "").await).await;
99524cf9f17catagen2026-10-10 10:53 UTC625
    assert_eq!(all["entries"][0]["ranked"], false);
99524cf9f17catagen2026-10-10 10:53 UTC626
}
99524cf9f17catagen2026-10-10 10:53 UTC627
99524cf9f17catagen2026-10-10 10:53 UTC628
#[tokio::test]
782069860236atagen2026-10-10 12:44 UTC629
async fn idle_runs_are_evicted_and_the_server_has_a_limit() {
782069860236atagen2026-10-10 12:44 UTC630
    let forgetful = crate::app(Config {
782069860236atagen2026-10-10 12:44 UTC631
        idle_ttl: Duration::ZERO,
782069860236atagen2026-10-10 12:44 UTC632
        ..Config::default()
782069860236atagen2026-10-10 12:44 UTC633
    });
782069860236atagen2026-10-10 12:44 UTC634
    let first = send(&forgetful, "POST", "/runs", &[], "").await;
782069860236atagen2026-10-10 12:44 UTC635
    let location = header(&first, "location").to_owned();
782069860236atagen2026-10-10 12:44 UTC636
    send(&forgetful, "POST", "/runs", &[], "").await;
782069860236atagen2026-10-10 12:44 UTC637
    let gone = send(&forgetful, "GET", &location, &[], "").await;
782069860236atagen2026-10-10 12:44 UTC638
    assert_eq!(gone.status(), StatusCode::NOT_FOUND);
782069860236atagen2026-10-10 12:44 UTC639
782069860236atagen2026-10-10 12:44 UTC640
    let tiny = crate::app(Config {
782069860236atagen2026-10-10 12:44 UTC641
        max_runs: 1,
782069860236atagen2026-10-10 12:44 UTC642
        ..Config::default()
782069860236atagen2026-10-10 12:44 UTC643
    });
782069860236atagen2026-10-10 12:44 UTC644
    send(&tiny, "POST", "/runs", &[], "").await;
782069860236atagen2026-10-10 12:44 UTC645
    let full = send(&tiny, "POST", "/runs", &[], "").await;
782069860236atagen2026-10-10 12:44 UTC646
    assert_eq!(full.status(), StatusCode::SERVICE_UNAVAILABLE);
782069860236atagen2026-10-10 12:44 UTC647
}
782069860236atagen2026-10-10 12:44 UTC648
782069860236atagen2026-10-10 12:44 UTC649
#[tokio::test]
782069860236atagen2026-10-10 12:44 UTC650
async fn shame_from_two_runs_adds_up() {
782069860236atagen2026-10-10 12:44 UTC651
    let state = Arc::new(AppState::default());
782069860236atagen2026-10-10 12:44 UTC652
    state.insert(fixture());
782069860236atagen2026-10-10 12:44 UTC653
    state.insert(Run::new("deadbeef".into(), "dead".into(), 7));
782069860236atagen2026-10-10 12:44 UTC654
    let app = router(state);
782069860236atagen2026-10-10 12:44 UTC655
    let first = send(&app, "TRACE", "/runs/cafebabe", &[], "").await;
782069860236atagen2026-10-10 12:44 UTC656
    assert_eq!(header(&first, "x-shame"), "1");
782069860236atagen2026-10-10 12:44 UTC657
    let cookie = header(&first, "set-cookie")
782069860236atagen2026-10-10 12:44 UTC658
        .split(';')
782069860236atagen2026-10-10 12:44 UTC659
        .next()
782069860236atagen2026-10-10 12:44 UTC660
        .unwrap()
782069860236atagen2026-10-10 12:44 UTC661
        .to_owned();
782069860236atagen2026-10-10 12:44 UTC662
    let second = send(&app, "TRACE", "/runs/deadbeef", &[("cookie", &cookie)], "").await;
782069860236atagen2026-10-10 12:44 UTC663
    assert_eq!(header(&second, "x-shame"), "2");
782069860236atagen2026-10-10 12:44 UTC664
}
782069860236atagen2026-10-10 12:44 UTC665
782069860236atagen2026-10-10 12:44 UTC666
#[tokio::test]
99524cf9f17catagen2026-10-10 10:53 UTC667
async fn the_root_negotiates() {
782069860236atagen2026-10-10 12:44 UTC668
    let app = app();
99524cf9f17catagen2026-10-10 10:53 UTC669
    let browser = send(&app, "GET", "/", &[("accept", "text/html,*/*")], "").await;
99524cf9f17catagen2026-10-10 10:53 UTC670
    assert_eq!(browser.status(), StatusCode::SEE_OTHER);
782069860236atagen2026-10-10 12:44 UTC671
    assert_eq!(header(&browser, "location"), "/ui/");
99524cf9f17catagen2026-10-10 10:53 UTC672
    let curl = send(&app, "GET", "/", &[("accept", "*/*")], "").await;
99524cf9f17catagen2026-10-10 10:53 UTC673
    assert_eq!(json(curl).await["ui"], "/ui/");
99524cf9f17catagen2026-10-10 10:53 UTC674
    let page = send(&app, "GET", "/ui/", &[], "").await;
99524cf9f17catagen2026-10-10 10:53 UTC675
    assert_eq!(page.status(), StatusCode::OK);
99524cf9f17catagen2026-10-10 10:53 UTC676
}
99524cf9f17catagen2026-10-10 10:53 UTC677
99524cf9f17catagen2026-10-10 10:53 UTC678
#[tokio::test]
99524cf9f17catagen2026-10-10 10:53 UTC679
async fn the_ui_replays_what_browsers_cannot_send() {
782069860236atagen2026-10-10 12:44 UTC680
    let app = app();
99524cf9f17catagen2026-10-10 10:53 UTC681
    let envelope =
99524cf9f17catagen2026-10-10 10:53 UTC682
        r#"{"method": "TRACE", "path": "/graveyard", "headers": [["X-Confession", "I did it"]]}"#;
99524cf9f17catagen2026-10-10 10:53 UTC683
    let traced = json(send(&app, "POST", "/ui/send", &[], envelope).await).await;
99524cf9f17catagen2026-10-10 10:53 UTC684
    assert_eq!(traced["status"], 405);
99524cf9f17catagen2026-10-10 10:53 UTC685
    let envelope = r#"{"method": "GET", "path": "/", "headers": [["Accept", "text/html"]]}"#;
99524cf9f17catagen2026-10-10 10:53 UTC686
    let redirected = json(send(&app, "POST", "/ui/send", &[], envelope).await).await;
99524cf9f17catagen2026-10-10 10:53 UTC687
    assert_eq!(redirected["status"], 303);
99524cf9f17catagen2026-10-10 10:53 UTC688
    assert_eq!(redirected["reason"], "See Other");
99524cf9f17catagen2026-10-10 10:53 UTC689
}
c3e0c27cf8fdatagen2026-10-10 11:28 UTC690
c3e0c27cf8fdatagen2026-10-10 11:28 UTC691
#[tokio::test]
ba879f96510datagen2026-10-10 12:03 UTC692
async fn the_ui_decodes_gzip_for_display() {
ba879f96510datagen2026-10-10 12:03 UTC693
    let app = seeded_with(Floor::Bedroom, |run| run.items.ring = true);
ba879f96510datagen2026-10-10 12:03 UTC694
    let envelope =
ba879f96510datagen2026-10-10 12:03 UTC695
        r#"{"method": "GET", "path": "/runs/cafebabe", "headers": [["Accept-Encoding", "gzip"]]}"#;
ba879f96510datagen2026-10-10 12:03 UTC696
    let shown = json(send(&app, "POST", "/ui/send", &[], envelope).await).await;
ba879f96510datagen2026-10-10 12:03 UTC697
    assert!(
ba879f96510datagen2026-10-10 12:03 UTC698
        shown["body"]
ba879f96510datagen2026-10-10 12:03 UTC699
            .as_str()
ba879f96510datagen2026-10-10 12:03 UTC700
            .unwrap()
782069860236atagen2026-10-10 12:44 UTC701
            .contains("\"public_id\":\"cafe\"")
ba879f96510datagen2026-10-10 12:03 UTC702
    );
ba879f96510datagen2026-10-10 12:03 UTC703
    let encoding = shown["headers"]
ba879f96510datagen2026-10-10 12:03 UTC704
        .as_array()
ba879f96510datagen2026-10-10 12:03 UTC705
        .unwrap()
ba879f96510datagen2026-10-10 12:03 UTC706
        .iter()
ba879f96510datagen2026-10-10 12:03 UTC707
        .any(|h| h[0] == "content-encoding" && h[1] == "gzip");
ba879f96510datagen2026-10-10 12:03 UTC708
    assert!(encoding);
ba879f96510datagen2026-10-10 12:03 UTC709
}
782069860236atagen2026-10-10 12:44 UTC710
782069860236atagen2026-10-10 12:44 UTC711
#[tokio::test]
782069860236atagen2026-10-10 12:44 UTC712
async fn http_1_0_gets_the_pdf_treatment() {
782069860236atagen2026-10-10 12:44 UTC713
    let request = Request::builder()
782069860236atagen2026-10-10 12:44 UTC714
        .uri("/graveyard")
782069860236atagen2026-10-10 12:44 UTC715
        .version(Version::HTTP_10)
782069860236atagen2026-10-10 12:44 UTC716
        .body(Body::empty())
782069860236atagen2026-10-10 12:44 UTC717
        .unwrap();
782069860236atagen2026-10-10 12:44 UTC718
    let response = app().oneshot(request).await.unwrap();
782069860236atagen2026-10-10 12:44 UTC719
    assert_eq!(response.status(), StatusCode::HTTP_VERSION_NOT_SUPPORTED);
782069860236atagen2026-10-10 12:44 UTC720
}
ef7d6dee5976atagen2026-10-10 13:23 UTC721
ef7d6dee5976atagen2026-10-10 13:23 UTC722
#[tokio::test]
ef7d6dee5976atagen2026-10-10 13:23 UTC723
async fn mom_speaks_american() {
ef7d6dee5976atagen2026-10-10 13:23 UTC724
    let app = seeded_with(Floor::Nans, mum_at_the_door);
ef7d6dee5976atagen2026-10-10 13:23 UTC725
    let us = [("accept-language", "en-US")];
ef7d6dee5976atagen2026-10-10 13:23 UTC726
    let knock = send(&app, "GET", "/runs/cafebabe", &us, "").await;
ef7d6dee5976atagen2026-10-10 13:23 UTC727
    assert_eq!(header(&knock, "content-language"), "en-US");
ef7d6dee5976atagen2026-10-10 13:23 UTC728
    assert_eq!(header(&knock, "via"), "1.1 mom");
ef7d6dee5976atagen2026-10-10 13:23 UTC729
    assert!(
ef7d6dee5976atagen2026-10-10 13:23 UTC730
        json(knock).await["detail"]
ef7d6dee5976atagen2026-10-10 13:23 UTC731
            .as_str()
ef7d6dee5976atagen2026-10-10 13:23 UTC732
            .unwrap()
ef7d6dee5976atagen2026-10-10 13:23 UTC733
            .contains("Someone is at the door.")
ef7d6dee5976atagen2026-10-10 13:23 UTC734
    );
ef7d6dee5976atagen2026-10-10 13:23 UTC735
    let excuse = [
ef7d6dee5976atagen2026-10-10 13:23 UTC736
        ("accept-language", "en-US"),
ef7d6dee5976atagen2026-10-10 13:23 UTC737
        (
ef7d6dee5976atagen2026-10-10 13:23 UTC738
            "authorization",
ef7d6dee5976atagen2026-10-10 13:23 UTC739
            "Digest username=\"carving the turkey\", nonce=\"abc\"",
ef7d6dee5976atagen2026-10-10 13:23 UTC740
        ),
ef7d6dee5976atagen2026-10-10 13:23 UTC741
    ];
ef7d6dee5976atagen2026-10-10 13:23 UTC742
    let answered = send(&app, "GET", "/runs/cafebabe", &excuse, "").await;
ef7d6dee5976atagen2026-10-10 13:23 UTC743
    assert_eq!(answered.status(), StatusCode::OK);
ef7d6dee5976atagen2026-10-10 13:23 UTC744
    let floor = json(send(&app, "GET", "/runs/cafebabe/floor", &us, "").await).await;
ef7d6dee5976atagen2026-10-10 13:23 UTC745
    assert_eq!(floor["name"], "Grandma's house for Thanksgiving");
ef7d6dee5976atagen2026-10-10 13:23 UTC746
}
ef7d6dee5976atagen2026-10-10 13:23 UTC747
ef7d6dee5976atagen2026-10-10 13:23 UTC748
#[tokio::test]
ef7d6dee5976atagen2026-10-10 13:23 UTC749
async fn the_default_locale_is_british() {
ef7d6dee5976atagen2026-10-10 13:23 UTC750
    let (app, _) = seeded(Floor::Nans);
ef7d6dee5976atagen2026-10-10 13:23 UTC751
    let floor = send(&app, "GET", "/runs/cafebabe/floor", &[], "").await;
ef7d6dee5976atagen2026-10-10 13:23 UTC752
    assert_eq!(header(&floor, "content-language"), "en-GB");
ef7d6dee5976atagen2026-10-10 13:23 UTC753
    assert!(header(&floor, "vary").contains("Accept-Language"));
ef7d6dee5976atagen2026-10-10 13:23 UTC754
    assert_eq!(json(floor).await["name"], "Nan's house at Christmas");
ef7d6dee5976atagen2026-10-10 13:23 UTC755
}
58e1fb3943e6atagen2026-10-10 13:30 UTC756
58e1fb3943e6atagen2026-10-10 13:30 UTC757
fn advertised(response: &Response) -> Vec<String> {
58e1fb3943e6atagen2026-10-10 13:30 UTC758
    header(response, "link")
58e1fb3943e6atagen2026-10-10 13:30 UTC759
        .split(", <")
58e1fb3943e6atagen2026-10-10 13:30 UTC760
        .filter(|entry| entry.contains("rels/technique"))
58e1fb3943e6atagen2026-10-10 13:30 UTC761
        .filter_map(|entry| {
58e1fb3943e6atagen2026-10-10 13:30 UTC762
            entry
58e1fb3943e6atagen2026-10-10 13:30 UTC763
                .split("title=\"")
58e1fb3943e6atagen2026-10-10 13:30 UTC764
                .nth(1)?
58e1fb3943e6atagen2026-10-10 13:30 UTC765
                .split('"')
58e1fb3943e6atagen2026-10-10 13:30 UTC766
                .next()
58e1fb3943e6atagen2026-10-10 13:30 UTC767
                .map(str::to_owned)
58e1fb3943e6atagen2026-10-10 13:30 UTC768
        })
58e1fb3943e6atagen2026-10-10 13:30 UTC769
        .collect()
58e1fb3943e6atagen2026-10-10 13:30 UTC770
}
58e1fb3943e6atagen2026-10-10 13:30 UTC771
58e1fb3943e6atagen2026-10-10 13:30 UTC772
#[tokio::test]
58e1fb3943e6atagen2026-10-10 13:30 UTC773
async fn links_offer_only_usable_techniques() {
58e1fb3943e6atagen2026-10-10 13:30 UTC774
    let app = seeded_with(Floor::Bedroom, |run| run.foreskin = Some(1.0));
58e1fb3943e6atagen2026-10-10 13:30 UTC775
    let cold = send(&app, "GET", "/runs/cafebabe", &[], "").await;
58e1fb3943e6atagen2026-10-10 13:30 UTC776
    assert_eq!(advertised(&cold), ["warmup", "ice"]);
58e1fb3943e6atagen2026-10-10 13:30 UTC777
    let warm = send(&app, "JELQ", "/runs/cafebabe?technique=warmup", &[], "").await;
58e1fb3943e6atagen2026-10-10 13:30 UTC778
    let usable = advertised(&warm);
58e1fb3943e6atagen2026-10-10 13:30 UTC779
    assert!(usable.contains(&"wet".to_owned()) && usable.contains(&"dry".to_owned()));
58e1fb3943e6atagen2026-10-10 13:30 UTC780
    assert!(!usable.contains(&"uli".to_owned()), "uli needs the forum");
58e1fb3943e6atagen2026-10-10 13:30 UTC781
    assert!(!usable.contains(&"pump".to_owned()), "pump needs a pump");
58e1fb3943e6atagen2026-10-10 13:30 UTC782
    assert!(
58e1fb3943e6atagen2026-10-10 13:30 UTC783
        header(&warm, "link").contains("title=\"wet\"; allow=\"JELQ\"; lubricant=\"required\"")
58e1fb3943e6atagen2026-10-10 13:30 UTC784
    );
58e1fb3943e6atagen2026-10-10 13:30 UTC785
58e1fb3943e6atagen2026-10-10 13:30 UTC786
    let (forum, _) = seeded(Floor::Forum);
58e1fb3943e6atagen2026-10-10 13:30 UTC787
    let response = send(&forum, "GET", "/runs/cafebabe", &[], "").await;
58e1fb3943e6atagen2026-10-10 13:30 UTC788
    assert!(advertised(&response).is_empty());
58e1fb3943e6atagen2026-10-10 13:30 UTC789
}
d604371299fbatagen2026-10-10 13:54 UTC790
d604371299fbatagen2026-10-10 13:54 UTC791
fn linked_rels(response: &Response) -> Vec<String> {
d604371299fbatagen2026-10-10 13:54 UTC792
    header(response, "link")
d604371299fbatagen2026-10-10 13:54 UTC793
        .split(", <")
d604371299fbatagen2026-10-10 13:54 UTC794
        .filter_map(|entry| {
d604371299fbatagen2026-10-10 13:54 UTC795
            entry
d604371299fbatagen2026-10-10 13:54 UTC796
                .split("rels/")
d604371299fbatagen2026-10-10 13:54 UTC797
                .nth(1)?
d604371299fbatagen2026-10-10 13:54 UTC798
                .split('"')
d604371299fbatagen2026-10-10 13:54 UTC799
                .next()
d604371299fbatagen2026-10-10 13:54 UTC800
                .map(str::to_owned)
d604371299fbatagen2026-10-10 13:54 UTC801
        })
d604371299fbatagen2026-10-10 13:54 UTC802
        .filter(|rel| rel != "technique")
d604371299fbatagen2026-10-10 13:54 UTC803
        .collect()
d604371299fbatagen2026-10-10 13:54 UTC804
}
d604371299fbatagen2026-10-10 13:54 UTC805
d604371299fbatagen2026-10-10 13:54 UTC806
#[tokio::test]
d604371299fbatagen2026-10-10 13:54 UTC807
async fn links_hide_resources_that_would_do_nothing() {
d604371299fbatagen2026-10-10 13:54 UTC808
    let (bedroom, _) = seeded(Floor::Bedroom);
d604371299fbatagen2026-10-10 13:54 UTC809
    let before = linked_rels(&send(&bedroom, "GET", "/runs/cafebabe", &[], "").await);
d604371299fbatagen2026-10-10 13:54 UTC810
    assert!(before.contains(&"drawer".to_owned()));
d604371299fbatagen2026-10-10 13:54 UTC811
    assert!(!before.contains(&"diagnosis".to_owned()));
d604371299fbatagen2026-10-10 13:54 UTC812
    assert!(!before.contains(&"thread".to_owned()));
d604371299fbatagen2026-10-10 13:54 UTC813
    let searched = send(&bedroom, "POST", "/runs/cafebabe/drawer", &[], "").await;
d604371299fbatagen2026-10-10 13:54 UTC814
    assert!(!linked_rels(&searched).contains(&"drawer".to_owned()));
d604371299fbatagen2026-10-10 13:54 UTC815
d604371299fbatagen2026-10-10 13:54 UTC816
    let (clinic, _) = seeded(Floor::Clinic);
d604371299fbatagen2026-10-10 13:54 UTC817
    let rels = linked_rels(&send(&clinic, "GET", "/runs/cafebabe", &[], "").await);
d604371299fbatagen2026-10-10 13:54 UTC818
    assert!(rels.contains(&"diagnosis".to_owned()) && rels.contains(&"thread".to_owned()));
d604371299fbatagen2026-10-10 13:54 UTC819
}
0d604ee39751atagen2026-10-10 14:15 UTC820
0d604ee39751atagen2026-10-10 14:15 UTC821
#[tokio::test]
0d604ee39751atagen2026-10-10 14:15 UTC822
async fn ignoring_mum_has_consequences() {
0d604ee39751atagen2026-10-10 14:15 UTC823
    let app = seeded_with(Floor::Bedroom, |run| {
0d604ee39751atagen2026-10-10 14:15 UTC824
        mum_at_the_door(run);
0d604ee39751atagen2026-10-10 14:15 UTC825
        run.here.turns = 6;
0d604ee39751atagen2026-10-10 14:15 UTC826
    });
0d604ee39751atagen2026-10-10 14:15 UTC827
    let knock = send(&app, "GET", "/runs/cafebabe", &[], "").await;
0d604ee39751atagen2026-10-10 14:15 UTC828
    assert_eq!(
0d604ee39751atagen2026-10-10 14:15 UTC829
        json(knock).await["type"],
0d604ee39751atagen2026-10-10 14:15 UTC830
        "https://jelq.dev/problems/someone-at-the-door"
0d604ee39751atagen2026-10-10 14:15 UTC831
    );
0d604ee39751atagen2026-10-10 14:15 UTC832
    let leave = send(&app, "POST", "/runs/cafebabe/floor", &[], "").await;
0d604ee39751atagen2026-10-10 14:15 UTC833
    assert_eq!(
0d604ee39751atagen2026-10-10 14:15 UTC834
        leave.status(),
0d604ee39751atagen2026-10-10 14:15 UTC835
        StatusCode::UNAUTHORIZED,
0d604ee39751atagen2026-10-10 14:15 UTC836
        "no leaving with Mum at the door"
0d604ee39751atagen2026-10-10 14:15 UTC837
    );
0d604ee39751atagen2026-10-10 14:15 UTC838
    send(&app, "GET", "/runs/cafebabe", &[], "").await;
0d604ee39751atagen2026-10-10 14:15 UTC839
    let opened = send(&app, "GET", "/runs/cafebabe", &[], "").await;
0d604ee39751atagen2026-10-10 14:15 UTC840
    assert_eq!(opened.status(), StatusCode::GONE);
0d604ee39751atagen2026-10-10 14:15 UTC841
    assert_eq!(
0d604ee39751atagen2026-10-10 14:15 UTC842
        json(opened).await["type"],
0d604ee39751atagen2026-10-10 14:15 UTC843
        "https://jelq.dev/problems/opened"
0d604ee39751atagen2026-10-10 14:15 UTC844
    );
0d604ee39751atagen2026-10-10 14:15 UTC845
}
0d604ee39751atagen2026-10-10 14:15 UTC846
0d604ee39751atagen2026-10-10 14:15 UTC847
#[tokio::test]
0d604ee39751atagen2026-10-10 14:15 UTC848
async fn caged_drawers_are_noisy_and_eventually_give_up_the_key() {
0d604ee39751atagen2026-10-10 14:15 UTC849
    let app = seeded_with(Floor::ChangingRoom, |run| run.items.cage = true);
0d604ee39751atagen2026-10-10 14:15 UTC850
    let mut statuses = Vec::new();
0d604ee39751atagen2026-10-10 14:15 UTC851
    for _ in 0..12 {
0d604ee39751atagen2026-10-10 14:15 UTC852
        let response = send(&app, "POST", "/runs/cafebabe/drawer", &[], "").await;
0d604ee39751atagen2026-10-10 14:15 UTC853
        statuses.push(response.status());
0d604ee39751atagen2026-10-10 14:15 UTC854
        if response.status() == StatusCode::OK {
0d604ee39751atagen2026-10-10 14:15 UTC855
            break;
0d604ee39751atagen2026-10-10 14:15 UTC856
        }
0d604ee39751atagen2026-10-10 14:15 UTC857
    }
0d604ee39751atagen2026-10-10 14:15 UTC858
    assert_eq!(statuses.last(), Some(&StatusCode::OK), "{statuses:?}");
0d604ee39751atagen2026-10-10 14:15 UTC859
    assert!(
0d604ee39751atagen2026-10-10 14:15 UTC860
        statuses[..statuses.len() - 1]
0d604ee39751atagen2026-10-10 14:15 UTC861
            .iter()
0d604ee39751atagen2026-10-10 14:15 UTC862
            .all(|s| *s == StatusCode::NO_CONTENT)
0d604ee39751atagen2026-10-10 14:15 UTC863
    );
0d604ee39751atagen2026-10-10 14:15 UTC864
}
f39b10e20a0batagen2026-10-10 14:33 UTC865
f39b10e20a0batagen2026-10-10 14:33 UTC866
#[tokio::test]
f39b10e20a0batagen2026-10-10 14:33 UTC867
async fn the_ruler_replaces_the_self_report() {
f39b10e20a0batagen2026-10-10 14:33 UTC868
    let app = seeded_with(Floor::Bedroom, |_| {});
f39b10e20a0batagen2026-10-10 14:33 UTC869
    let before = send(&app, "GET", "/runs/cafebabe", &[], "").await;
f39b10e20a0batagen2026-10-10 14:33 UTC870
    assert!(before.headers().contains_key("x-self-reported-length"));
f39b10e20a0batagen2026-10-10 14:33 UTC871
    assert!(!before.headers().contains_key("x-length"));
f39b10e20a0batagen2026-10-10 14:33 UTC872
f39b10e20a0batagen2026-10-10 14:33 UTC873
    let app = seeded_with(Floor::Bedroom, |run| run.items.ruler = true);
f39b10e20a0batagen2026-10-10 14:33 UTC874
    let after = send(&app, "GET", "/runs/cafebabe", &[], "").await;
f39b10e20a0batagen2026-10-10 14:33 UTC875
    assert!(!after.headers().contains_key("x-self-reported-length"));
f39b10e20a0batagen2026-10-10 14:33 UTC876
    assert!(after.headers().contains_key("x-length"));
f39b10e20a0batagen2026-10-10 14:33 UTC877
}
d46f9d0b953aatagen2026-10-10 14:41 UTC878
d46f9d0b953aatagen2026-10-10 14:41 UTC879
#[tokio::test]
d46f9d0b953aatagen2026-10-10 14:41 UTC880
async fn an_open_tab_is_linked_and_jelq_still_answers_with_choices() {
d46f9d0b953aatagen2026-10-10 14:41 UTC881
    let app = seeded_with(Floor::Bathroom, |run| {
d46f9d0b953aatagen2026-10-10 14:41 UTC882
        run.story = Story::Pending;
d46f9d0b953aatagen2026-10-10 14:41 UTC883
        run.warmed_up = true;
d46f9d0b953aatagen2026-10-10 14:41 UTC884
    });
d46f9d0b953aatagen2026-10-10 14:41 UTC885
    let response = send(&app, "GET", "/runs/cafebabe", &[], "").await;
d46f9d0b953aatagen2026-10-10 14:41 UTC886
    let links = header(&response, "link");
d46f9d0b953aatagen2026-10-10 14:41 UTC887
    assert!(links.contains("/branches/forum>; rel=\"https://jelq.dev/rels/branch\""));
d46f9d0b953aatagen2026-10-10 14:41 UTC888
    assert!(links.contains("/branches/close-tab>; rel=\"https://jelq.dev/rels/branch\""));
d46f9d0b953aatagen2026-10-10 14:41 UTC889
    assert!(links.contains("?technique=wet>"));
d46f9d0b953aatagen2026-10-10 14:41 UTC890
    let jelq = send(&app, "JELQ", "/runs/cafebabe?technique=edge", &[], "").await;
d46f9d0b953aatagen2026-10-10 14:41 UTC891
    assert_eq!(jelq.status(), StatusCode::MULTIPLE_CHOICES);
d46f9d0b953aatagen2026-10-10 14:41 UTC892
}
d46f9d0b953aatagen2026-10-10 14:41 UTC893
d46f9d0b953aatagen2026-10-10 14:41 UTC894
#[tokio::test]
d46f9d0b953aatagen2026-10-10 14:41 UTC895
async fn either_story_choice_is_reachable_during_cooldown() {
d46f9d0b953aatagen2026-10-10 14:41 UTC896
    for choice in ["forum", "close-tab"] {
d46f9d0b953aatagen2026-10-10 14:41 UTC897
        let app = seeded_with(Floor::Bathroom, |run| {
d46f9d0b953aatagen2026-10-10 14:41 UTC898
            run.turn = 9;
d46f9d0b953aatagen2026-10-10 14:41 UTC899
            run.warmed_up = true;
d46f9d0b953aatagen2026-10-10 14:41 UTC900
            run.stamina = 1;
d46f9d0b953aatagen2026-10-10 14:41 UTC901
            run.items.door_locks = 1;
d46f9d0b953aatagen2026-10-10 14:41 UTC902
        });
d46f9d0b953aatagen2026-10-10 14:41 UTC903
        let tenth = send(&app, "JELQ", "/runs/cafebabe?technique=edge", &[], "").await;
d46f9d0b953aatagen2026-10-10 14:41 UTC904
        assert_eq!(tenth.status(), StatusCode::OK);
d46f9d0b953aatagen2026-10-10 14:41 UTC905
        let response = send(&app, "GET", "/runs/cafebabe", &[], "").await;
d46f9d0b953aatagen2026-10-10 14:41 UTC906
        assert!(!header(&response, "link").contains("?technique="));
d46f9d0b953aatagen2026-10-10 14:41 UTC907
        let path = format!("/runs/cafebabe/branches/{choice}");
d46f9d0b953aatagen2026-10-10 14:41 UTC908
        assert!(
d46f9d0b953aatagen2026-10-10 14:41 UTC909
            header(&response, "link")
d46f9d0b953aatagen2026-10-10 14:41 UTC910
                .contains(&format!("<{path}>; rel=\"https://jelq.dev/rels/branch\""))
d46f9d0b953aatagen2026-10-10 14:41 UTC911
        );
d46f9d0b953aatagen2026-10-10 14:41 UTC912
        let chosen = send(&app, "POST", &path, &[], "").await;
d46f9d0b953aatagen2026-10-10 14:41 UTC913
        assert_eq!(chosen.status(), StatusCode::SEE_OTHER);
d46f9d0b953aatagen2026-10-10 14:41 UTC914
        assert!(!header(&chosen, "link").contains("/branches/"));
d46f9d0b953aatagen2026-10-10 14:41 UTC915
        let repeated = send(&app, "POST", &path, &[], "").await;
d46f9d0b953aatagen2026-10-10 14:41 UTC916
        assert_eq!(repeated.status(), StatusCode::CONFLICT);
d46f9d0b953aatagen2026-10-10 14:41 UTC917
    }
d46f9d0b953aatagen2026-10-10 14:41 UTC918
}
d46f9d0b953aatagen2026-10-10 14:41 UTC919
d46f9d0b953aatagen2026-10-10 14:41 UTC920
#[tokio::test]
d46f9d0b953aatagen2026-10-10 14:41 UTC921
async fn a_door_lock_says_when_it_was_used() {
d46f9d0b953aatagen2026-10-10 14:41 UTC922
    let app = seeded_with(Floor::Bathroom, |run| {
d46f9d0b953aatagen2026-10-10 14:41 UTC923
        run.items.door_locks = 1;
d46f9d0b953aatagen2026-10-10 14:41 UTC924
        run.suspicion = SUSPICION_LIMIT;
d46f9d0b953aatagen2026-10-10 14:41 UTC925
        run.warmed_up = true;
d46f9d0b953aatagen2026-10-10 14:41 UTC926
    });
d46f9d0b953aatagen2026-10-10 14:41 UTC927
    let response = send(&app, "JELQ", "/runs/cafebabe?technique=dry", &[], "").await;
d46f9d0b953aatagen2026-10-10 14:41 UTC928
    assert_ne!(response.status(), StatusCode::CONFLICT);
d46f9d0b953aatagen2026-10-10 14:41 UTC929
    assert!(
d46f9d0b953aatagen2026-10-10 14:41 UTC930
        response
d46f9d0b953aatagen2026-10-10 14:41 UTC931
            .headers()
d46f9d0b953aatagen2026-10-10 14:41 UTC932
            .get_all("warning")
d46f9d0b953aatagen2026-10-10 14:41 UTC933
            .iter()
7d3fb63208bfatagen2026-10-11 04:38 UTC934
            .any(|w| w.to_str().unwrap().contains("your brother tried the door"))
d46f9d0b953aatagen2026-10-10 14:41 UTC935
    );
d46f9d0b953aatagen2026-10-10 14:41 UTC936
}
7d3fb63208bfatagen2026-10-11 04:38 UTC937
7d3fb63208bfatagen2026-10-11 04:38 UTC938
#[tokio::test]
7d3fb63208bfatagen2026-10-11 04:38 UTC939
async fn shopping_survives_browsing_but_not_jelqing() {
7d3fb63208bfatagen2026-10-11 04:38 UTC940
    let app = seeded_with(Floor::ChangingRoom, |run| run.cash = 50);
7d3fb63208bfatagen2026-10-11 04:38 UTC941
    send(&app, "GET", "/runs/cafebabe/shop", &[], "").await;
7d3fb63208bfatagen2026-10-11 04:38 UTC942
    let view = send(&app, "GET", "/runs/cafebabe", &[], "").await;
7d3fb63208bfatagen2026-10-11 04:38 UTC943
    assert!(header(&view, "link").contains("shop-item"));
7d3fb63208bfatagen2026-10-11 04:38 UTC944
    let jelq = send(&app, "JELQ", "/runs/cafebabe?technique=warmup", &[], "").await;
7d3fb63208bfatagen2026-10-11 04:38 UTC945
    assert_ne!(jelq.status(), StatusCode::METHOD_NOT_ALLOWED);
7d3fb63208bfatagen2026-10-11 04:38 UTC946
    let after = send(&app, "GET", "/runs/cafebabe", &[], "").await;
7d3fb63208bfatagen2026-10-11 04:38 UTC947
    assert!(!header(&after, "link").contains("shop-item"));
7d3fb63208bfatagen2026-10-11 04:38 UTC948
}