atagen

jelq

public

an http based roguelike rpg about jelqing

Star0
Watch0
585e6b7ce08636105da83cdbed5ff24679d0db4d
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,
9683736a2c5eatagen2026-10-10 13:15 UTC14
    game::{Floor, MumVisit, Run},
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,
782069860236atagen2026-10-10 12:44 UTC86
    });
782069860236atagen2026-10-10 12:44 UTC87
}
782069860236atagen2026-10-10 12:44 UTC88
a9b016e4b248atagen2026-10-10 08:12 UTC89
#[tokio::test]
a9b016e4b248atagen2026-10-10 08:12 UTC90
async fn creating_a_run_returns_location_and_etag() {
782069860236atagen2026-10-10 12:44 UTC91
    let response = send(&app(), "POST", "/runs?seed=1", &[], "").await;
a9b016e4b248atagen2026-10-10 08:12 UTC92
    assert_eq!(response.status(), StatusCode::CREATED);
782069860236atagen2026-10-10 12:44 UTC93
    assert!(header(&response, "location").starts_with("/runs/"));
a9b016e4b248atagen2026-10-10 08:12 UTC94
    assert!(response.headers().contains_key(header::ETAG));
782069860236atagen2026-10-10 12:44 UTC95
    assert_eq!(header(&response, "server"), "jelqd/0.6.9");
782069860236atagen2026-10-10 12:44 UTC96
}
782069860236atagen2026-10-10 12:44 UTC97
782069860236atagen2026-10-10 12:44 UTC98
#[tokio::test]
782069860236atagen2026-10-10 12:44 UTC99
async fn bodies_do_not_repeat_the_headers() {
782069860236atagen2026-10-10 12:44 UTC100
    let (app, _) = seeded(Floor::Bedroom);
782069860236atagen2026-10-10 12:44 UTC101
    let response = send(&app, "GET", "/runs/cafebabe", &[], "").await;
782069860236atagen2026-10-10 12:44 UTC102
    assert!(response.headers().contains_key("x-eq"));
782069860236atagen2026-10-10 12:44 UTC103
    let body = json(response).await;
782069860236atagen2026-10-10 12:44 UTC104
    for stat in [
782069860236atagen2026-10-10 12:44 UTC105
        "eq",
782069860236atagen2026-10-10 12:44 UTC106
        "girth",
782069860236atagen2026-10-10 12:44 UTC107
        "foreskin",
782069860236atagen2026-10-10 12:44 UTC108
        "stamina",
782069860236atagen2026-10-10 12:44 UTC109
        "cash",
782069860236atagen2026-10-10 12:44 UTC110
        "self_reported_length",
782069860236atagen2026-10-10 12:44 UTC111
    ] {
782069860236atagen2026-10-10 12:44 UTC112
        assert!(body.get(stat).is_none(), "{stat} is already a header");
782069860236atagen2026-10-10 12:44 UTC113
    }
a9b016e4b248atagen2026-10-10 08:12 UTC114
}
a9b016e4b248atagen2026-10-10 08:12 UTC115
a9b016e4b248atagen2026-10-10 08:12 UTC116
#[tokio::test]
a9b016e4b248atagen2026-10-10 08:12 UTC117
async fn jelqing_cold_fails_the_precondition() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC118
    let (app, _) = seeded(Floor::Bedroom);
a9b016e4b248atagen2026-10-10 08:12 UTC119
    let response = send(&app, "JELQ", "/runs/cafebabe?technique=stretch", &[], "").await;
a9b016e4b248atagen2026-10-10 08:12 UTC120
    assert_eq!(response.status(), StatusCode::PRECONDITION_FAILED);
a9b016e4b248atagen2026-10-10 08:12 UTC121
    assert_eq!(
782069860236atagen2026-10-10 12:44 UTC122
        header(&response, "content-type"),
a9b016e4b248atagen2026-10-10 08:12 UTC123
        "application/problem+json"
a9b016e4b248atagen2026-10-10 08:12 UTC124
    );
a9b016e4b248atagen2026-10-10 08:12 UTC125
}
a9b016e4b248atagen2026-10-10 08:12 UTC126
a9b016e4b248atagen2026-10-10 08:12 UTC127
#[tokio::test]
a9b016e4b248atagen2026-10-10 08:12 UTC128
async fn wet_without_lube_is_unsupported_media() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC129
    let (app, _) = seeded(Floor::Bedroom);
a9b016e4b248atagen2026-10-10 08:12 UTC130
    send(&app, "JELQ", "/runs/cafebabe?technique=warmup", &[], "").await;
a9b016e4b248atagen2026-10-10 08:12 UTC131
    let response = send(&app, "JELQ", "/runs/cafebabe?technique=wet", &[], "").await;
a9b016e4b248atagen2026-10-10 08:12 UTC132
    assert_eq!(response.status(), StatusCode::UNSUPPORTED_MEDIA_TYPE);
a9b016e4b248atagen2026-10-10 08:12 UTC133
    let images = send(
a9b016e4b248atagen2026-10-10 08:12 UTC134
        &app,
a9b016e4b248atagen2026-10-10 08:12 UTC135
        "JELQ",
a9b016e4b248atagen2026-10-10 08:12 UTC136
        "/runs/cafebabe?technique=wet",
a9b016e4b248atagen2026-10-10 08:12 UTC137
        &[("content-type", "image/jpeg")],
a9b016e4b248atagen2026-10-10 08:12 UTC138
        "",
a9b016e4b248atagen2026-10-10 08:12 UTC139
    )
a9b016e4b248atagen2026-10-10 08:12 UTC140
    .await;
a9b016e4b248atagen2026-10-10 08:12 UTC141
    assert_eq!(json(images).await["detail"], "No.");
a9b016e4b248atagen2026-10-10 08:12 UTC142
}
a9b016e4b248atagen2026-10-10 08:12 UTC143
a9b016e4b248atagen2026-10-10 08:12 UTC144
#[tokio::test]
a9b016e4b248atagen2026-10-10 08:12 UTC145
async fn unknown_methods_are_not_implemented() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC146
    let (app, _) = seeded(Floor::Bedroom);
a9b016e4b248atagen2026-10-10 08:12 UTC147
    let response = send(&app, "MILK", "/runs/cafebabe", &[], "").await;
a9b016e4b248atagen2026-10-10 08:12 UTC148
    assert_eq!(response.status(), StatusCode::NOT_IMPLEMENTED);
782069860236atagen2026-10-10 12:44 UTC149
    let teapot = send(&app, "BREW", "/runs/cafebabe", &[], "").await;
782069860236atagen2026-10-10 12:44 UTC150
    assert_eq!(teapot.status(), StatusCode::IM_A_TEAPOT);
782069860236atagen2026-10-10 12:44 UTC151
}
782069860236atagen2026-10-10 12:44 UTC152
782069860236atagen2026-10-10 12:44 UTC153
#[tokio::test]
782069860236atagen2026-10-10 12:44 UTC154
async fn the_clinic_does_not_advertise_jelq() {
782069860236atagen2026-10-10 12:44 UTC155
    let (app, _) = seeded(Floor::Clinic);
782069860236atagen2026-10-10 12:44 UTC156
    let options = send(&app, "OPTIONS", "/runs/cafebabe", &[], "").await;
782069860236atagen2026-10-10 12:44 UTC157
    assert!(!header(&options, "allow").contains("JELQ"));
782069860236atagen2026-10-10 12:44 UTC158
    let jelq = send(&app, "JELQ", "/runs/cafebabe?technique=wet", &[], "").await;
782069860236atagen2026-10-10 12:44 UTC159
    assert_eq!(jelq.status(), StatusCode::METHOD_NOT_ALLOWED);
782069860236atagen2026-10-10 12:44 UTC160
    assert!(!header(&jelq, "allow").contains("JELQ"));
782069860236atagen2026-10-10 12:44 UTC161
    let (bedroom, _) = seeded(Floor::Bedroom);
782069860236atagen2026-10-10 12:44 UTC162
    let options = send(&bedroom, "OPTIONS", "/runs/cafebabe", &[], "").await;
782069860236atagen2026-10-10 12:44 UTC163
    assert!(header(&options, "allow").contains("JELQ"));
a9b016e4b248atagen2026-10-10 08:12 UTC164
}
a9b016e4b248atagen2026-10-10 08:12 UTC165
a9b016e4b248atagen2026-10-10 08:12 UTC166
#[tokio::test]
a9b016e4b248atagen2026-10-10 08:12 UTC167
async fn patch_only_accepts_the_truth() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC168
    let (app, _) = seeded(Floor::Bedroom);
a9b016e4b248atagen2026-10-10 08:12 UTC169
    let patch = [("content-type", "application/json-patch+json")];
a9b016e4b248atagen2026-10-10 08:12 UTC170
    let replace = send(
a9b016e4b248atagen2026-10-10 08:12 UTC171
        &app,
a9b016e4b248atagen2026-10-10 08:12 UTC172
        "PATCH",
a9b016e4b248atagen2026-10-10 08:12 UTC173
        "/runs/cafebabe",
a9b016e4b248atagen2026-10-10 08:12 UTC174
        &patch,
a9b016e4b248atagen2026-10-10 08:12 UTC175
        r#"[{"op":"replace","path":"/length","value":12}]"#,
a9b016e4b248atagen2026-10-10 08:12 UTC176
    )
a9b016e4b248atagen2026-10-10 08:12 UTC177
    .await;
a9b016e4b248atagen2026-10-10 08:12 UTC178
    assert_eq!(replace.status(), StatusCode::UNPROCESSABLE_ENTITY);
a9b016e4b248atagen2026-10-10 08:12 UTC179
    let lie = send(
a9b016e4b248atagen2026-10-10 08:12 UTC180
        &app,
a9b016e4b248atagen2026-10-10 08:12 UTC181
        "PATCH",
a9b016e4b248atagen2026-10-10 08:12 UTC182
        "/runs/cafebabe",
a9b016e4b248atagen2026-10-10 08:12 UTC183
        &patch,
a9b016e4b248atagen2026-10-10 08:12 UTC184
        r#"[{"op":"test","path":"/length","value":40}]"#,
a9b016e4b248atagen2026-10-10 08:12 UTC185
    )
a9b016e4b248atagen2026-10-10 08:12 UTC186
    .await;
a9b016e4b248atagen2026-10-10 08:12 UTC187
    assert_eq!(
a9b016e4b248atagen2026-10-10 08:12 UTC188
        json(lie).await["type"],
a9b016e4b248atagen2026-10-10 08:12 UTC189
        "https://jelq.dev/problems/exaggeration"
a9b016e4b248atagen2026-10-10 08:12 UTC190
    );
a9b016e4b248atagen2026-10-10 08:12 UTC191
}
a9b016e4b248atagen2026-10-10 08:12 UTC192
a9b016e4b248atagen2026-10-10 08:12 UTC193
#[tokio::test]
a9b016e4b248atagen2026-10-10 08:12 UTC194
async fn deleting_the_foreskin_requires_certainty() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC195
    let (app, _) = seeded(Floor::Bedroom);
a9b016e4b248atagen2026-10-10 08:12 UTC196
    let response = send(&app, "DELETE", "/runs/cafebabe/foreskin", &[], "").await;
a9b016e4b248atagen2026-10-10 08:12 UTC197
    assert_eq!(response.status(), StatusCode::PRECONDITION_REQUIRED);
782069860236atagen2026-10-10 12:44 UTC198
    let look = send(&app, "GET", "/runs/cafebabe/foreskin", &[], "").await;
782069860236atagen2026-10-10 12:44 UTC199
    assert_eq!(look.status(), StatusCode::NO_CONTENT);
782069860236atagen2026-10-10 12:44 UTC200
    let etag = header(&look, "etag").to_owned();
a9b016e4b248atagen2026-10-10 08:12 UTC201
    let response = send(
a9b016e4b248atagen2026-10-10 08:12 UTC202
        &app,
a9b016e4b248atagen2026-10-10 08:12 UTC203
        "DELETE",
a9b016e4b248atagen2026-10-10 08:12 UTC204
        "/runs/cafebabe/foreskin",
a9b016e4b248atagen2026-10-10 08:12 UTC205
        &[("if-match", &etag)],
a9b016e4b248atagen2026-10-10 08:12 UTC206
        "",
a9b016e4b248atagen2026-10-10 08:12 UTC207
    )
a9b016e4b248atagen2026-10-10 08:12 UTC208
    .await;
a9b016e4b248atagen2026-10-10 08:12 UTC209
    assert_eq!(response.status(), StatusCode::NO_CONTENT);
a9b016e4b248atagen2026-10-10 08:12 UTC210
    let response = send(&app, "GET", "/runs/cafebabe/foreskin", &[], "").await;
a9b016e4b248atagen2026-10-10 08:12 UTC211
    assert_eq!(response.status(), StatusCode::GONE);
a9b016e4b248atagen2026-10-10 08:12 UTC212
}
a9b016e4b248atagen2026-10-10 08:12 UTC213
a9b016e4b248atagen2026-10-10 08:12 UTC214
#[tokio::test]
a9b016e4b248atagen2026-10-10 08:12 UTC215
async fn length_is_chunked_until_the_ruler() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC216
    let (app, _) = seeded(Floor::Bedroom);
a9b016e4b248atagen2026-10-10 08:12 UTC217
    let lie = send(&app, "GET", "/runs/cafebabe/length", &[], "").await;
a9b016e4b248atagen2026-10-10 08:12 UTC218
    assert!(!lie.headers().contains_key(header::CONTENT_LENGTH));
a9b016e4b248atagen2026-10-10 08:12 UTC219
    let found = send(&app, "POST", "/runs/cafebabe/drawer", &[], "").await;
a9b016e4b248atagen2026-10-10 08:12 UTC220
    assert_eq!(found.status(), StatusCode::FOUND);
a9b016e4b248atagen2026-10-10 08:12 UTC221
    let truth = send(&app, "GET", "/runs/cafebabe/length", &[], "").await;
782069860236atagen2026-10-10 12:44 UTC222
    let declared: usize = header(&truth, "content-length").parse().unwrap();
782069860236atagen2026-10-10 12:44 UTC223
    let body = text(truth).await;
a9b016e4b248atagen2026-10-10 08:12 UTC224
    assert_eq!(body.len(), declared);
782069860236atagen2026-10-10 12:44 UTC225
    assert!(body.starts_with("8=") && body.ends_with('D'));
782069860236atagen2026-10-10 12:44 UTC226
}
782069860236atagen2026-10-10 12:44 UTC227
782069860236atagen2026-10-10 12:44 UTC228
#[tokio::test]
782069860236atagen2026-10-10 12:44 UTC229
async fn length_supports_inch_ranges() {
782069860236atagen2026-10-10 12:44 UTC230
    let (app, _) = seeded(Floor::Bedroom);
782069860236atagen2026-10-10 12:44 UTC231
    let response = send(
782069860236atagen2026-10-10 12:44 UTC232
        &app,
782069860236atagen2026-10-10 12:44 UTC233
        "GET",
782069860236atagen2026-10-10 12:44 UTC234
        "/runs/cafebabe/length",
782069860236atagen2026-10-10 12:44 UTC235
        &[("range", "inches=0-1")],
782069860236atagen2026-10-10 12:44 UTC236
        "",
782069860236atagen2026-10-10 12:44 UTC237
    )
782069860236atagen2026-10-10 12:44 UTC238
    .await;
782069860236atagen2026-10-10 12:44 UTC239
    assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT);
782069860236atagen2026-10-10 12:44 UTC240
    assert!(header(&response, "content-range").starts_with("inches 0.00-1.00/"));
782069860236atagen2026-10-10 12:44 UTC241
    assert_eq!(text(response).await.len(), 25);
782069860236atagen2026-10-10 12:44 UTC242
    let absurd = send(
782069860236atagen2026-10-10 12:44 UTC243
        &app,
782069860236atagen2026-10-10 12:44 UTC244
        "GET",
782069860236atagen2026-10-10 12:44 UTC245
        "/runs/cafebabe/length",
782069860236atagen2026-10-10 12:44 UTC246
        &[("range", "inches=40-50")],
782069860236atagen2026-10-10 12:44 UTC247
        "",
782069860236atagen2026-10-10 12:44 UTC248
    )
782069860236atagen2026-10-10 12:44 UTC249
    .await;
782069860236atagen2026-10-10 12:44 UTC250
    assert_eq!(absurd.status(), StatusCode::RANGE_NOT_SATISFIABLE);
a9b016e4b248atagen2026-10-10 08:12 UTC251
}
a9b016e4b248atagen2026-10-10 08:12 UTC252
a9b016e4b248atagen2026-10-10 08:12 UTC253
#[tokio::test]
a9b016e4b248atagen2026-10-10 08:12 UTC254
async fn the_clinic_escalates_toward_honesty() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC255
    let (app, _) = seeded(Floor::Clinic);
a9b016e4b248atagen2026-10-10 08:12 UTC256
    let mut hints = Vec::new();
a9b016e4b248atagen2026-10-10 08:12 UTC257
    for _ in 0..3 {
a9b016e4b248atagen2026-10-10 08:12 UTC258
        let response = send(
a9b016e4b248atagen2026-10-10 08:12 UTC259
            &app,
a9b016e4b248atagen2026-10-10 08:12 UTC260
            "GET",
a9b016e4b248atagen2026-10-10 08:12 UTC261
            "/runs/cafebabe/diagnosis",
a9b016e4b248atagen2026-10-10 08:12 UTC262
            &[("authorization", "Excuse \"changing\"")],
a9b016e4b248atagen2026-10-10 08:12 UTC263
            "",
a9b016e4b248atagen2026-10-10 08:12 UTC264
        )
a9b016e4b248atagen2026-10-10 08:12 UTC265
        .await;
a9b016e4b248atagen2026-10-10 08:12 UTC266
        assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
782069860236atagen2026-10-10 12:44 UTC267
        hints.push(header(&response, "www-authenticate").to_owned());
a9b016e4b248atagen2026-10-10 08:12 UTC268
    }
a9b016e4b248atagen2026-10-10 08:12 UTC269
    assert!(hints[2].contains("Be honest."));
a9b016e4b248atagen2026-10-10 08:12 UTC270
}
a9b016e4b248atagen2026-10-10 08:12 UTC271
a9b016e4b248atagen2026-10-10 08:12 UTC272
#[tokio::test]
a9b016e4b248atagen2026-10-10 08:12 UTC273
async fn the_true_ending_is_not_modified() {
782069860236atagen2026-10-10 12:44 UTC274
    let (early, _) = seeded(Floor::Bedroom);
a9b016e4b248atagen2026-10-10 08:12 UTC275
    let response = send(&early, "GET", "/runs/cafebabe/diagnosis", &[], "").await;
a9b016e4b248atagen2026-10-10 08:12 UTC276
    assert_eq!(response.status(), StatusCode::TOO_EARLY);
a9b016e4b248atagen2026-10-10 08:12 UTC277
782069860236atagen2026-10-10 12:44 UTC278
    let (app, etag) = seeded(Floor::Clinic);
782069860236atagen2026-10-10 12:44 UTC279
    assert!(
782069860236atagen2026-10-10 12:44 UTC280
        !etag.contains("cafebabe"),
782069860236atagen2026-10-10 12:44 UTC281
        "the origin ETag must not be derivable from the id"
782069860236atagen2026-10-10 12:44 UTC282
    );
a9b016e4b248atagen2026-10-10 08:12 UTC283
    let response = send(
a9b016e4b248atagen2026-10-10 08:12 UTC284
        &app,
a9b016e4b248atagen2026-10-10 08:12 UTC285
        "GET",
a9b016e4b248atagen2026-10-10 08:12 UTC286
        "/runs/cafebabe/diagnosis",
a9b016e4b248atagen2026-10-10 08:12 UTC287
        &[
a9b016e4b248atagen2026-10-10 08:12 UTC288
            ("authorization", "Bearer honesty"),
a9b016e4b248atagen2026-10-10 08:12 UTC289
            ("if-none-match", &etag),
a9b016e4b248atagen2026-10-10 08:12 UTC290
        ],
a9b016e4b248atagen2026-10-10 08:12 UTC291
        "",
a9b016e4b248atagen2026-10-10 08:12 UTC292
    )
a9b016e4b248atagen2026-10-10 08:12 UTC293
    .await;
a9b016e4b248atagen2026-10-10 08:12 UTC294
    assert_eq!(response.status(), StatusCode::NOT_MODIFIED);
782069860236atagen2026-10-10 12:44 UTC295
    assert_eq!(header(&response, "x-diagnosis"), "normal");
a9b016e4b248atagen2026-10-10 08:12 UTC296
}
a9b016e4b248atagen2026-10-10 08:12 UTC297
a9b016e4b248atagen2026-10-10 08:12 UTC298
#[tokio::test]
9683736a2c5eatagen2026-10-10 13:15 UTC299
async fn mum_accepts_a_known_excuse() {
9683736a2c5eatagen2026-10-10 13:15 UTC300
    let app = seeded_with(Floor::Bedroom, mum_at_the_door);
a9b016e4b248atagen2026-10-10 08:12 UTC301
    let response = send(&app, "GET", "/runs/cafebabe", &[], "").await;
a9b016e4b248atagen2026-10-10 08:12 UTC302
    assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
9683736a2c5eatagen2026-10-10 13:15 UTC303
    assert_eq!(header(&response, "via"), "1.1 mum");
a9b016e4b248atagen2026-10-10 08:12 UTC304
    let response = send(
a9b016e4b248atagen2026-10-10 08:12 UTC305
        &app,
a9b016e4b248atagen2026-10-10 08:12 UTC306
        "GET",
a9b016e4b248atagen2026-10-10 08:12 UTC307
        "/runs/cafebabe",
a9b016e4b248atagen2026-10-10 08:12 UTC308
        &[("authorization", "Excuse \"homework\"")],
a9b016e4b248atagen2026-10-10 08:12 UTC309
        "",
a9b016e4b248atagen2026-10-10 08:12 UTC310
    )
a9b016e4b248atagen2026-10-10 08:12 UTC311
    .await;
a9b016e4b248atagen2026-10-10 08:12 UTC312
    assert_eq!(response.status(), StatusCode::OK);
a9b016e4b248atagen2026-10-10 08:12 UTC313
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC314
782069860236atagen2026-10-10 12:44 UTC315
#[tokio::test]
9683736a2c5eatagen2026-10-10 13:15 UTC316
async fn mum_runs_out_of_patience() {
9683736a2c5eatagen2026-10-10 13:15 UTC317
    let app = seeded_with(Floor::Bedroom, mum_at_the_door);
782069860236atagen2026-10-10 12:44 UTC318
    let lie = [("authorization", "Excuse \"it is for science\"")];
782069860236atagen2026-10-10 12:44 UTC319
    for _ in 0..2 {
782069860236atagen2026-10-10 12:44 UTC320
        let response = send(&app, "GET", "/runs/cafebabe", &lie, "").await;
782069860236atagen2026-10-10 12:44 UTC321
        assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
782069860236atagen2026-10-10 12:44 UTC322
    }
782069860236atagen2026-10-10 12:44 UTC323
    let caught = send(&app, "GET", "/runs/cafebabe", &lie, "").await;
782069860236atagen2026-10-10 12:44 UTC324
    assert_eq!(caught.status(), StatusCode::GONE);
782069860236atagen2026-10-10 12:44 UTC325
    assert_eq!(
782069860236atagen2026-10-10 12:44 UTC326
        json(caught).await["type"],
782069860236atagen2026-10-10 12:44 UTC327
        "https://jelq.dev/problems/caught"
782069860236atagen2026-10-10 12:44 UTC328
    );
782069860236atagen2026-10-10 12:44 UTC329
}
782069860236atagen2026-10-10 12:44 UTC330
782069860236atagen2026-10-10 12:44 UTC331
#[tokio::test]
9683736a2c5eatagen2026-10-10 13:15 UTC332
async fn mum_remembers_excuses_by_christmas() {
9683736a2c5eatagen2026-10-10 13:15 UTC333
    let app = seeded_with(Floor::Nans, |run| {
9683736a2c5eatagen2026-10-10 13:15 UTC334
        mum_at_the_door(run);
782069860236atagen2026-10-10 12:44 UTC335
        run.heard.push("homework".into());
782069860236atagen2026-10-10 12:44 UTC336
    });
782069860236atagen2026-10-10 12:44 UTC337
    let basic = send(
782069860236atagen2026-10-10 12:44 UTC338
        &app,
782069860236atagen2026-10-10 12:44 UTC339
        "GET",
782069860236atagen2026-10-10 12:44 UTC340
        "/runs/cafebabe",
782069860236atagen2026-10-10 12:44 UTC341
        &[("authorization", "Excuse \"changing\"")],
782069860236atagen2026-10-10 12:44 UTC342
        "",
782069860236atagen2026-10-10 12:44 UTC343
    )
782069860236atagen2026-10-10 12:44 UTC344
    .await;
782069860236atagen2026-10-10 12:44 UTC345
    assert_eq!(basic.status(), StatusCode::UNAUTHORIZED);
782069860236atagen2026-10-10 12:44 UTC346
    assert!(header(&basic, "www-authenticate").starts_with("Digest"));
782069860236atagen2026-10-10 12:44 UTC347
    let replay = send(
782069860236atagen2026-10-10 12:44 UTC348
        &app,
782069860236atagen2026-10-10 12:44 UTC349
        "GET",
782069860236atagen2026-10-10 12:44 UTC350
        "/runs/cafebabe",
782069860236atagen2026-10-10 12:44 UTC351
        &[(
782069860236atagen2026-10-10 12:44 UTC352
            "authorization",
782069860236atagen2026-10-10 12:44 UTC353
            "Digest username=\"homework\", nonce=\"abc\"",
782069860236atagen2026-10-10 12:44 UTC354
        )],
782069860236atagen2026-10-10 12:44 UTC355
        "",
782069860236atagen2026-10-10 12:44 UTC356
    )
782069860236atagen2026-10-10 12:44 UTC357
    .await;
782069860236atagen2026-10-10 12:44 UTC358
    assert_eq!(
782069860236atagen2026-10-10 12:44 UTC359
        json(replay).await["type"],
782069860236atagen2026-10-10 12:44 UTC360
        "https://jelq.dev/problems/excuse-replayed"
782069860236atagen2026-10-10 12:44 UTC361
    );
782069860236atagen2026-10-10 12:44 UTC362
    let fresh = send(
782069860236atagen2026-10-10 12:44 UTC363
        &app,
782069860236atagen2026-10-10 12:44 UTC364
        "GET",
782069860236atagen2026-10-10 12:44 UTC365
        "/runs/cafebabe",
782069860236atagen2026-10-10 12:44 UTC366
        &[(
782069860236atagen2026-10-10 12:44 UTC367
            "authorization",
9683736a2c5eatagen2026-10-10 13:15 UTC368
            "Digest username=\"wrapping presents\", nonce=\"abc\"",
782069860236atagen2026-10-10 12:44 UTC369
        )],
782069860236atagen2026-10-10 12:44 UTC370
        "",
782069860236atagen2026-10-10 12:44 UTC371
    )
782069860236atagen2026-10-10 12:44 UTC372
    .await;
782069860236atagen2026-10-10 12:44 UTC373
    assert_eq!(fresh.status(), StatusCode::OK);
782069860236atagen2026-10-10 12:44 UTC374
}
782069860236atagen2026-10-10 12:44 UTC375
782069860236atagen2026-10-10 12:44 UTC376
#[tokio::test]
782069860236atagen2026-10-10 12:44 UTC377
async fn a_pulled_muscle_is_a_true_excuse() {
9683736a2c5eatagen2026-10-10 13:15 UTC378
    let app = seeded_with(Floor::Nans, |run| {
9683736a2c5eatagen2026-10-10 13:15 UTC379
        mum_at_the_door(run);
782069860236atagen2026-10-10 12:44 UTC380
        run.conditions.pulled_muscle = true;
782069860236atagen2026-10-10 12:44 UTC381
        run.heard.push("pulled a muscle".into());
782069860236atagen2026-10-10 12:44 UTC382
    });
782069860236atagen2026-10-10 12:44 UTC383
    let response = send(
782069860236atagen2026-10-10 12:44 UTC384
        &app,
782069860236atagen2026-10-10 12:44 UTC385
        "GET",
782069860236atagen2026-10-10 12:44 UTC386
        "/runs/cafebabe",
782069860236atagen2026-10-10 12:44 UTC387
        &[(
782069860236atagen2026-10-10 12:44 UTC388
            "authorization",
782069860236atagen2026-10-10 12:44 UTC389
            "Digest username=\"pulled a muscle\", nonce=\"abc\"",
782069860236atagen2026-10-10 12:44 UTC390
        )],
782069860236atagen2026-10-10 12:44 UTC391
        "",
782069860236atagen2026-10-10 12:44 UTC392
    )
782069860236atagen2026-10-10 12:44 UTC393
    .await;
782069860236atagen2026-10-10 12:44 UTC394
    assert_eq!(response.status(), StatusCode::OK);
782069860236atagen2026-10-10 12:44 UTC395
    assert!(header(&response, "x-stamina").ends_with("/7"));
dfa38cf4cd4batagen2026-10-10 09:17 UTC396
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC397
dfa38cf4cd4batagen2026-10-10 09:17 UTC398
#[tokio::test]
dfa38cf4cd4batagen2026-10-10 09:17 UTC399
async fn floors_end_when_they_end() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC400
    let app = seeded_with(Floor::Bedroom, |_| {});
dfa38cf4cd4batagen2026-10-10 09:17 UTC401
    let early = send(&app, "POST", "/runs/cafebabe/floor", &[], "").await;
dfa38cf4cd4batagen2026-10-10 09:17 UTC402
    assert_eq!(early.status(), StatusCode::TOO_EARLY);
782069860236atagen2026-10-10 12:44 UTC403
    let app = seeded_with(Floor::Bedroom, |run| run.here.turns = 6);
dfa38cf4cd4batagen2026-10-10 09:17 UTC404
    let next = send(&app, "POST", "/runs/cafebabe/floor", &[], "").await;
782069860236atagen2026-10-10 12:44 UTC405
    assert!(
782069860236atagen2026-10-10 12:44 UTC406
        next.status() == StatusCode::SEE_OTHER || next.status() == StatusCode::CONFLICT,
9683736a2c5eatagen2026-10-10 13:15 UTC407
        "either the floor changed or the zip did"
782069860236atagen2026-10-10 12:44 UTC408
    );
dfa38cf4cd4batagen2026-10-10 09:17 UTC409
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC410
dfa38cf4cd4batagen2026-10-10 09:17 UTC411
#[tokio::test]
dfa38cf4cd4batagen2026-10-10 09:17 UTC412
async fn pump_bro_upsells() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC413
    let closed = seeded_with(Floor::Bedroom, |_| {});
dfa38cf4cd4batagen2026-10-10 09:17 UTC414
    let response = send(&closed, "GET", "/runs/cafebabe/shop", &[], "").await;
dfa38cf4cd4batagen2026-10-10 09:17 UTC415
    assert_eq!(response.status(), StatusCode::NOT_FOUND);
dfa38cf4cd4batagen2026-10-10 09:17 UTC416
9683736a2c5eatagen2026-10-10 13:15 UTC417
    let app = seeded_with(Floor::ChangingRoom, |_| {});
dfa38cf4cd4batagen2026-10-10 09:17 UTC418
    let pump = send(&app, "POST", "/runs/cafebabe/shop/pump", &[], "").await;
dfa38cf4cd4batagen2026-10-10 09:17 UTC419
    assert_eq!(pump.status(), StatusCode::UPGRADE_REQUIRED);
782069860236atagen2026-10-10 12:44 UTC420
    assert_eq!(header(&pump, "upgrade"), "pump-pro/2.0");
dfa38cf4cd4batagen2026-10-10 09:17 UTC421
    let pro = send(&app, "POST", "/runs/cafebabe/shop/pump-pro", &[], "").await;
dfa38cf4cd4batagen2026-10-10 09:17 UTC422
    assert_eq!(pro.status(), StatusCode::CREATED);
dfa38cf4cd4batagen2026-10-10 09:17 UTC423
    let broke = send(&app, "POST", "/runs/cafebabe/shop/extender", &[], "").await;
dfa38cf4cd4batagen2026-10-10 09:17 UTC424
    assert_eq!(broke.status(), StatusCode::PAYMENT_REQUIRED);
dfa38cf4cd4batagen2026-10-10 09:17 UTC425
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC426
dfa38cf4cd4batagen2026-10-10 09:17 UTC427
#[tokio::test]
782069860236atagen2026-10-10 12:44 UTC428
async fn only_the_key_opens_the_cage() {
9683736a2c5eatagen2026-10-10 13:15 UTC429
    let app = seeded_with(Floor::ChangingRoom, |run| run.items.cage = true);
782069860236atagen2026-10-10 12:44 UTC430
    send(&app, "POST", "/runs/cafebabe/shop/ice-pack", &[], "").await;
782069860236atagen2026-10-10 12:44 UTC431
    let still = send(
782069860236atagen2026-10-10 12:44 UTC432
        &app,
782069860236atagen2026-10-10 12:44 UTC433
        "GET",
782069860236atagen2026-10-10 12:44 UTC434
        "/runs/cafebabe/inventory/chastity-cage",
782069860236atagen2026-10-10 12:44 UTC435
        &[],
782069860236atagen2026-10-10 12:44 UTC436
        "",
782069860236atagen2026-10-10 12:44 UTC437
    )
782069860236atagen2026-10-10 12:44 UTC438
    .await;
782069860236atagen2026-10-10 12:44 UTC439
    assert_eq!(still.status(), StatusCode::OK);
782069860236atagen2026-10-10 12:44 UTC440
    send(&app, "POST", "/runs/cafebabe/shop/cage-key", &[], "").await;
782069860236atagen2026-10-10 12:44 UTC441
    let gone = send(
782069860236atagen2026-10-10 12:44 UTC442
        &app,
782069860236atagen2026-10-10 12:44 UTC443
        "GET",
782069860236atagen2026-10-10 12:44 UTC444
        "/runs/cafebabe/inventory/chastity-cage",
782069860236atagen2026-10-10 12:44 UTC445
        &[],
782069860236atagen2026-10-10 12:44 UTC446
        "",
782069860236atagen2026-10-10 12:44 UTC447
    )
782069860236atagen2026-10-10 12:44 UTC448
    .await;
782069860236atagen2026-10-10 12:44 UTC449
    assert_eq!(gone.status(), StatusCode::NOT_FOUND);
782069860236atagen2026-10-10 12:44 UTC450
}
782069860236atagen2026-10-10 12:44 UTC451
782069860236atagen2026-10-10 12:44 UTC452
#[tokio::test]
dfa38cf4cd4batagen2026-10-10 09:17 UTC453
async fn combos_are_multi_status_xml() {
782069860236atagen2026-10-10 12:44 UTC454
    let app = seeded_with(Floor::Bathroom, |run| {
782069860236atagen2026-10-10 12:44 UTC455
        run.foreskin = Some(1.0);
782069860236atagen2026-10-10 12:44 UTC456
        run.items.blinds = true;
782069860236atagen2026-10-10 12:44 UTC457
    });
dfa38cf4cd4batagen2026-10-10 09:17 UTC458
    send(&app, "JELQ", "/runs/cafebabe?technique=warmup", &[], "").await;
dfa38cf4cd4batagen2026-10-10 09:17 UTC459
    let response = send(
dfa38cf4cd4batagen2026-10-10 09:17 UTC460
        &app,
dfa38cf4cd4batagen2026-10-10 09:17 UTC461
        "JELQ",
dfa38cf4cd4batagen2026-10-10 09:17 UTC462
        "/runs/cafebabe?technique=wet,stretch",
dfa38cf4cd4batagen2026-10-10 09:17 UTC463
        &[("content-type", "application/baby-oil")],
dfa38cf4cd4batagen2026-10-10 09:17 UTC464
        "",
dfa38cf4cd4batagen2026-10-10 09:17 UTC465
    )
dfa38cf4cd4batagen2026-10-10 09:17 UTC466
    .await;
dfa38cf4cd4batagen2026-10-10 09:17 UTC467
    assert_eq!(response.status(), StatusCode::MULTI_STATUS);
782069860236atagen2026-10-10 12:44 UTC468
    assert!(text(response).await.contains("<D:multistatus"));
dfa38cf4cd4batagen2026-10-10 09:17 UTC469
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC470
dfa38cf4cd4batagen2026-10-10 09:17 UTC471
#[tokio::test]
dfa38cf4cd4batagen2026-10-10 09:17 UTC472
async fn the_elder_is_stale() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC473
    let app = seeded_with(Floor::Forum, |run| run.length = 5.0);
dfa38cf4cd4batagen2026-10-10 09:17 UTC474
    let unverified = send(&app, "GET", "/runs/cafebabe/elder", &[], "").await;
dfa38cf4cd4batagen2026-10-10 09:17 UTC475
    assert_eq!(
dfa38cf4cd4batagen2026-10-10 09:17 UTC476
        unverified.status(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC477
        StatusCode::PROXY_AUTHENTICATION_REQUIRED
dfa38cf4cd4batagen2026-10-10 09:17 UTC478
    );
dfa38cf4cd4batagen2026-10-10 09:17 UTC479
    let lie = send(
dfa38cf4cd4batagen2026-10-10 09:17 UTC480
        &app,
dfa38cf4cd4batagen2026-10-10 09:17 UTC481
        "GET",
dfa38cf4cd4batagen2026-10-10 09:17 UTC482
        "/runs/cafebabe/elder",
dfa38cf4cd4batagen2026-10-10 09:17 UTC483
        &[("proxy-authorization", "Ruler 9")],
dfa38cf4cd4batagen2026-10-10 09:17 UTC484
        "",
dfa38cf4cd4batagen2026-10-10 09:17 UTC485
    )
dfa38cf4cd4batagen2026-10-10 09:17 UTC486
    .await;
dfa38cf4cd4batagen2026-10-10 09:17 UTC487
    assert_eq!(lie.status(), StatusCode::BAD_REQUEST);
dfa38cf4cd4batagen2026-10-10 09:17 UTC488
    let csv = send(
dfa38cf4cd4batagen2026-10-10 09:17 UTC489
        &app,
dfa38cf4cd4batagen2026-10-10 09:17 UTC490
        "GET",
dfa38cf4cd4batagen2026-10-10 09:17 UTC491
        "/runs/cafebabe/elder",
dfa38cf4cd4batagen2026-10-10 09:17 UTC492
        &[("proxy-authorization", "Ruler 5.0")],
dfa38cf4cd4batagen2026-10-10 09:17 UTC493
        "",
dfa38cf4cd4batagen2026-10-10 09:17 UTC494
    )
dfa38cf4cd4batagen2026-10-10 09:17 UTC495
    .await;
dfa38cf4cd4batagen2026-10-10 09:17 UTC496
    assert_eq!(csv.status(), StatusCode::OK);
dfa38cf4cd4batagen2026-10-10 09:17 UTC497
    let stale = send(
dfa38cf4cd4batagen2026-10-10 09:17 UTC498
        &app,
dfa38cf4cd4batagen2026-10-10 09:17 UTC499
        "GET",
dfa38cf4cd4batagen2026-10-10 09:17 UTC500
        "/runs/cafebabe/elder",
dfa38cf4cd4batagen2026-10-10 09:17 UTC501
        &[("if-modified-since", "Mon, 15 Jun 2009 00:00:00 GMT")],
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!(stale.status(), StatusCode::NOT_MODIFIED);
782069860236atagen2026-10-10 12:44 UTC506
}
782069860236atagen2026-10-10 12:44 UTC507
782069860236atagen2026-10-10 12:44 UTC508
#[tokio::test]
782069860236atagen2026-10-10 12:44 UTC509
async fn the_displayed_length_is_accepted_as_honest() {
782069860236atagen2026-10-10 12:44 UTC510
    let app = seeded_with(Floor::Forum, |run| run.length = 5.2496);
782069860236atagen2026-10-10 12:44 UTC511
    let response = send(
782069860236atagen2026-10-10 12:44 UTC512
        &app,
782069860236atagen2026-10-10 12:44 UTC513
        "GET",
782069860236atagen2026-10-10 12:44 UTC514
        "/runs/cafebabe/elder",
782069860236atagen2026-10-10 12:44 UTC515
        &[("proxy-authorization", "Ruler 5.25")],
782069860236atagen2026-10-10 12:44 UTC516
        "",
782069860236atagen2026-10-10 12:44 UTC517
    )
782069860236atagen2026-10-10 12:44 UTC518
    .await;
782069860236atagen2026-10-10 12:44 UTC519
    assert_eq!(response.status(), StatusCode::OK);
dfa38cf4cd4batagen2026-10-10 09:17 UTC520
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC521
dfa38cf4cd4batagen2026-10-10 09:17 UTC522
#[tokio::test]
dfa38cf4cd4batagen2026-10-10 09:17 UTC523
async fn the_hotel_has_a_captive_portal() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC524
    let app = seeded_with(Floor::Hotel, |_| {});
dfa38cf4cd4batagen2026-10-10 09:17 UTC525
    let blocked = send(&app, "GET", "/runs/cafebabe", &[], "").await;
dfa38cf4cd4batagen2026-10-10 09:17 UTC526
    assert_eq!(
dfa38cf4cd4batagen2026-10-10 09:17 UTC527
        blocked.status(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC528
        StatusCode::NETWORK_AUTHENTICATION_REQUIRED
dfa38cf4cd4batagen2026-10-10 09:17 UTC529
    );
dfa38cf4cd4batagen2026-10-10 09:17 UTC530
    let accepted = send(&app, "POST", "/runs/cafebabe/portal", &[], "").await;
dfa38cf4cd4batagen2026-10-10 09:17 UTC531
    assert_eq!(accepted.status(), StatusCode::SEE_OTHER);
dfa38cf4cd4batagen2026-10-10 09:17 UTC532
    let online = send(&app, "GET", "/runs/cafebabe", &[], "").await;
dfa38cf4cd4batagen2026-10-10 09:17 UTC533
    assert_eq!(online.status(), StatusCode::OK);
dfa38cf4cd4batagen2026-10-10 09:17 UTC534
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC535
dfa38cf4cd4batagen2026-10-10 09:17 UTC536
#[tokio::test]
782069860236atagen2026-10-10 12:44 UTC537
async fn the_internet_never_forgets() {
782069860236atagen2026-10-10 12:44 UTC538
    let app = seeded_with(Floor::Bedroom, |run| {
782069860236atagen2026-10-10 12:44 UTC539
        run.filmed_at = Some(std::time::SystemTime::now());
dfa38cf4cd4batagen2026-10-10 09:17 UTC540
    });
782069860236atagen2026-10-10 12:44 UTC541
    let footage = send(&app, "GET", "/runs/cafebabe/footage", &[], "").await;
782069860236atagen2026-10-10 12:44 UTC542
    assert_eq!(footage.status(), StatusCode::OK);
99524cf9f17catagen2026-10-10 10:53 UTC543
    assert_eq!(
782069860236atagen2026-10-10 12:44 UTC544
        header(&footage, "cache-control"),
782069860236atagen2026-10-10 12:44 UTC545
        "public, max-age=31536000, immutable"
99524cf9f17catagen2026-10-10 10:53 UTC546
    );
782069860236atagen2026-10-10 12:44 UTC547
    assert!(footage.headers().contains_key(header::AGE));
782069860236atagen2026-10-10 12:44 UTC548
    let delete = send(&app, "DELETE", "/runs/cafebabe/footage", &[], "").await;
782069860236atagen2026-10-10 12:44 UTC549
    assert_eq!(delete.status(), StatusCode::METHOD_NOT_ALLOWED);
782069860236atagen2026-10-10 12:44 UTC550
    assert!(!header(&delete, "allow").contains("DELETE"));
99524cf9f17catagen2026-10-10 10:53 UTC551
}
99524cf9f17catagen2026-10-10 10:53 UTC552
99524cf9f17catagen2026-10-10 10:53 UTC553
#[tokio::test]
99524cf9f17catagen2026-10-10 10:53 UTC554
async fn forged_ruler_cookies_are_lies() {
99524cf9f17catagen2026-10-10 10:53 UTC555
    let forged = send(
782069860236atagen2026-10-10 12:44 UTC556
        &app(),
99524cf9f17catagen2026-10-10 10:53 UTC557
        "GET",
99524cf9f17catagen2026-10-10 10:53 UTC558
        "/leaderboard?sort=content-length",
99524cf9f17catagen2026-10-10 10:53 UTC559
        &[("cookie", "ruler=1")],
99524cf9f17catagen2026-10-10 10:53 UTC560
        "",
99524cf9f17catagen2026-10-10 10:53 UTC561
    )
99524cf9f17catagen2026-10-10 10:53 UTC562
    .await;
99524cf9f17catagen2026-10-10 10:53 UTC563
    assert_eq!(forged.status(), StatusCode::BAD_REQUEST);
99524cf9f17catagen2026-10-10 10:53 UTC564
}
99524cf9f17catagen2026-10-10 10:53 UTC565
99524cf9f17catagen2026-10-10 10:53 UTC566
#[tokio::test]
782069860236atagen2026-10-10 12:44 UTC567
async fn listings_never_reveal_the_private_id() {
782069860236atagen2026-10-10 12:44 UTC568
    let app = app();
782069860236atagen2026-10-10 12:44 UTC569
    let created = send(&app, "POST", "/runs", &[], "").await;
782069860236atagen2026-10-10 12:44 UTC570
    let location = header(&created, "location").to_owned();
782069860236atagen2026-10-10 12:44 UTC571
    let board = text(send(&app, "GET", "/leaderboard", &[], "").await).await;
782069860236atagen2026-10-10 12:44 UTC572
    let private = location.trim_start_matches("/runs/");
782069860236atagen2026-10-10 12:44 UTC573
    assert!(!board.contains(private));
782069860236atagen2026-10-10 12:44 UTC574
    let entry: Value = serde_json::from_str(&board).unwrap();
782069860236atagen2026-10-10 12:44 UTC575
    let public = entry["entries"][0]["public_id"].as_str().unwrap();
782069860236atagen2026-10-10 12:44 UTC576
    let guess = send(&app, "DELETE", &format!("/runs/{public}"), &[], "").await;
782069860236atagen2026-10-10 12:44 UTC577
    assert_eq!(guess.status(), StatusCode::NOT_FOUND);
782069860236atagen2026-10-10 12:44 UTC578
}
782069860236atagen2026-10-10 12:44 UTC579
782069860236atagen2026-10-10 12:44 UTC580
#[tokio::test]
99524cf9f17catagen2026-10-10 10:53 UTC581
async fn seeded_runs_are_unranked() {
782069860236atagen2026-10-10 12:44 UTC582
    let app = app();
99524cf9f17catagen2026-10-10 10:53 UTC583
    send(&app, "POST", "/runs?seed=7", &[], "").await;
99524cf9f17catagen2026-10-10 10:53 UTC584
    let board = json(send(&app, "GET", "/leaderboard", &[], "").await).await;
99524cf9f17catagen2026-10-10 10:53 UTC585
    assert_eq!(board["entries"].as_array().unwrap().len(), 0);
99524cf9f17catagen2026-10-10 10:53 UTC586
    let all = json(send(&app, "GET", "/leaderboard?seeded=include", &[], "").await).await;
99524cf9f17catagen2026-10-10 10:53 UTC587
    assert_eq!(all["entries"][0]["ranked"], false);
99524cf9f17catagen2026-10-10 10:53 UTC588
}
99524cf9f17catagen2026-10-10 10:53 UTC589
99524cf9f17catagen2026-10-10 10:53 UTC590
#[tokio::test]
782069860236atagen2026-10-10 12:44 UTC591
async fn idle_runs_are_evicted_and_the_server_has_a_limit() {
782069860236atagen2026-10-10 12:44 UTC592
    let forgetful = crate::app(Config {
782069860236atagen2026-10-10 12:44 UTC593
        idle_ttl: Duration::ZERO,
782069860236atagen2026-10-10 12:44 UTC594
        ..Config::default()
782069860236atagen2026-10-10 12:44 UTC595
    });
782069860236atagen2026-10-10 12:44 UTC596
    let first = send(&forgetful, "POST", "/runs", &[], "").await;
782069860236atagen2026-10-10 12:44 UTC597
    let location = header(&first, "location").to_owned();
782069860236atagen2026-10-10 12:44 UTC598
    send(&forgetful, "POST", "/runs", &[], "").await;
782069860236atagen2026-10-10 12:44 UTC599
    let gone = send(&forgetful, "GET", &location, &[], "").await;
782069860236atagen2026-10-10 12:44 UTC600
    assert_eq!(gone.status(), StatusCode::NOT_FOUND);
782069860236atagen2026-10-10 12:44 UTC601
782069860236atagen2026-10-10 12:44 UTC602
    let tiny = crate::app(Config {
782069860236atagen2026-10-10 12:44 UTC603
        max_runs: 1,
782069860236atagen2026-10-10 12:44 UTC604
        ..Config::default()
782069860236atagen2026-10-10 12:44 UTC605
    });
782069860236atagen2026-10-10 12:44 UTC606
    send(&tiny, "POST", "/runs", &[], "").await;
782069860236atagen2026-10-10 12:44 UTC607
    let full = send(&tiny, "POST", "/runs", &[], "").await;
782069860236atagen2026-10-10 12:44 UTC608
    assert_eq!(full.status(), StatusCode::SERVICE_UNAVAILABLE);
782069860236atagen2026-10-10 12:44 UTC609
}
782069860236atagen2026-10-10 12:44 UTC610
782069860236atagen2026-10-10 12:44 UTC611
#[tokio::test]
782069860236atagen2026-10-10 12:44 UTC612
async fn shame_from_two_runs_adds_up() {
782069860236atagen2026-10-10 12:44 UTC613
    let state = Arc::new(AppState::default());
782069860236atagen2026-10-10 12:44 UTC614
    state.insert(fixture());
782069860236atagen2026-10-10 12:44 UTC615
    state.insert(Run::new("deadbeef".into(), "dead".into(), 7));
782069860236atagen2026-10-10 12:44 UTC616
    let app = router(state);
782069860236atagen2026-10-10 12:44 UTC617
    let first = send(&app, "TRACE", "/runs/cafebabe", &[], "").await;
782069860236atagen2026-10-10 12:44 UTC618
    assert_eq!(header(&first, "x-shame"), "1");
782069860236atagen2026-10-10 12:44 UTC619
    let cookie = header(&first, "set-cookie")
782069860236atagen2026-10-10 12:44 UTC620
        .split(';')
782069860236atagen2026-10-10 12:44 UTC621
        .next()
782069860236atagen2026-10-10 12:44 UTC622
        .unwrap()
782069860236atagen2026-10-10 12:44 UTC623
        .to_owned();
782069860236atagen2026-10-10 12:44 UTC624
    let second = send(&app, "TRACE", "/runs/deadbeef", &[("cookie", &cookie)], "").await;
782069860236atagen2026-10-10 12:44 UTC625
    assert_eq!(header(&second, "x-shame"), "2");
782069860236atagen2026-10-10 12:44 UTC626
}
782069860236atagen2026-10-10 12:44 UTC627
782069860236atagen2026-10-10 12:44 UTC628
#[tokio::test]
99524cf9f17catagen2026-10-10 10:53 UTC629
async fn the_root_negotiates() {
782069860236atagen2026-10-10 12:44 UTC630
    let app = app();
99524cf9f17catagen2026-10-10 10:53 UTC631
    let browser = send(&app, "GET", "/", &[("accept", "text/html,*/*")], "").await;
99524cf9f17catagen2026-10-10 10:53 UTC632
    assert_eq!(browser.status(), StatusCode::SEE_OTHER);
782069860236atagen2026-10-10 12:44 UTC633
    assert_eq!(header(&browser, "location"), "/ui/");
99524cf9f17catagen2026-10-10 10:53 UTC634
    let curl = send(&app, "GET", "/", &[("accept", "*/*")], "").await;
99524cf9f17catagen2026-10-10 10:53 UTC635
    assert_eq!(json(curl).await["ui"], "/ui/");
99524cf9f17catagen2026-10-10 10:53 UTC636
    let page = send(&app, "GET", "/ui/", &[], "").await;
99524cf9f17catagen2026-10-10 10:53 UTC637
    assert_eq!(page.status(), StatusCode::OK);
99524cf9f17catagen2026-10-10 10:53 UTC638
}
99524cf9f17catagen2026-10-10 10:53 UTC639
99524cf9f17catagen2026-10-10 10:53 UTC640
#[tokio::test]
99524cf9f17catagen2026-10-10 10:53 UTC641
async fn the_ui_replays_what_browsers_cannot_send() {
782069860236atagen2026-10-10 12:44 UTC642
    let app = app();
99524cf9f17catagen2026-10-10 10:53 UTC643
    let envelope =
99524cf9f17catagen2026-10-10 10:53 UTC644
        r#"{"method": "TRACE", "path": "/graveyard", "headers": [["X-Confession", "I did it"]]}"#;
99524cf9f17catagen2026-10-10 10:53 UTC645
    let traced = json(send(&app, "POST", "/ui/send", &[], envelope).await).await;
99524cf9f17catagen2026-10-10 10:53 UTC646
    assert_eq!(traced["status"], 405);
99524cf9f17catagen2026-10-10 10:53 UTC647
    let envelope = r#"{"method": "GET", "path": "/", "headers": [["Accept", "text/html"]]}"#;
99524cf9f17catagen2026-10-10 10:53 UTC648
    let redirected = json(send(&app, "POST", "/ui/send", &[], envelope).await).await;
99524cf9f17catagen2026-10-10 10:53 UTC649
    assert_eq!(redirected["status"], 303);
99524cf9f17catagen2026-10-10 10:53 UTC650
    assert_eq!(redirected["reason"], "See Other");
99524cf9f17catagen2026-10-10 10:53 UTC651
}
c3e0c27cf8fdatagen2026-10-10 11:28 UTC652
c3e0c27cf8fdatagen2026-10-10 11:28 UTC653
#[tokio::test]
ba879f96510datagen2026-10-10 12:03 UTC654
async fn the_ui_decodes_gzip_for_display() {
ba879f96510datagen2026-10-10 12:03 UTC655
    let app = seeded_with(Floor::Bedroom, |run| run.items.ring = true);
ba879f96510datagen2026-10-10 12:03 UTC656
    let envelope =
ba879f96510datagen2026-10-10 12:03 UTC657
        r#"{"method": "GET", "path": "/runs/cafebabe", "headers": [["Accept-Encoding", "gzip"]]}"#;
ba879f96510datagen2026-10-10 12:03 UTC658
    let shown = json(send(&app, "POST", "/ui/send", &[], envelope).await).await;
ba879f96510datagen2026-10-10 12:03 UTC659
    assert!(
ba879f96510datagen2026-10-10 12:03 UTC660
        shown["body"]
ba879f96510datagen2026-10-10 12:03 UTC661
            .as_str()
ba879f96510datagen2026-10-10 12:03 UTC662
            .unwrap()
782069860236atagen2026-10-10 12:44 UTC663
            .contains("\"public_id\":\"cafe\"")
ba879f96510datagen2026-10-10 12:03 UTC664
    );
ba879f96510datagen2026-10-10 12:03 UTC665
    let encoding = shown["headers"]
ba879f96510datagen2026-10-10 12:03 UTC666
        .as_array()
ba879f96510datagen2026-10-10 12:03 UTC667
        .unwrap()
ba879f96510datagen2026-10-10 12:03 UTC668
        .iter()
ba879f96510datagen2026-10-10 12:03 UTC669
        .any(|h| h[0] == "content-encoding" && h[1] == "gzip");
ba879f96510datagen2026-10-10 12:03 UTC670
    assert!(encoding);
ba879f96510datagen2026-10-10 12:03 UTC671
}
782069860236atagen2026-10-10 12:44 UTC672
782069860236atagen2026-10-10 12:44 UTC673
#[tokio::test]
782069860236atagen2026-10-10 12:44 UTC674
async fn http_1_0_gets_the_pdf_treatment() {
782069860236atagen2026-10-10 12:44 UTC675
    let request = Request::builder()
782069860236atagen2026-10-10 12:44 UTC676
        .uri("/graveyard")
782069860236atagen2026-10-10 12:44 UTC677
        .version(Version::HTTP_10)
782069860236atagen2026-10-10 12:44 UTC678
        .body(Body::empty())
782069860236atagen2026-10-10 12:44 UTC679
        .unwrap();
782069860236atagen2026-10-10 12:44 UTC680
    let response = app().oneshot(request).await.unwrap();
782069860236atagen2026-10-10 12:44 UTC681
    assert_eq!(response.status(), StatusCode::HTTP_VERSION_NOT_SUPPORTED);
782069860236atagen2026-10-10 12:44 UTC682
}
ef7d6dee5976atagen2026-10-10 13:23 UTC683
ef7d6dee5976atagen2026-10-10 13:23 UTC684
#[tokio::test]
ef7d6dee5976atagen2026-10-10 13:23 UTC685
async fn mom_speaks_american() {
ef7d6dee5976atagen2026-10-10 13:23 UTC686
    let app = seeded_with(Floor::Nans, mum_at_the_door);
ef7d6dee5976atagen2026-10-10 13:23 UTC687
    let us = [("accept-language", "en-US")];
ef7d6dee5976atagen2026-10-10 13:23 UTC688
    let knock = send(&app, "GET", "/runs/cafebabe", &us, "").await;
ef7d6dee5976atagen2026-10-10 13:23 UTC689
    assert_eq!(header(&knock, "content-language"), "en-US");
ef7d6dee5976atagen2026-10-10 13:23 UTC690
    assert_eq!(header(&knock, "via"), "1.1 mom");
ef7d6dee5976atagen2026-10-10 13:23 UTC691
    assert!(
ef7d6dee5976atagen2026-10-10 13:23 UTC692
        json(knock).await["detail"]
ef7d6dee5976atagen2026-10-10 13:23 UTC693
            .as_str()
ef7d6dee5976atagen2026-10-10 13:23 UTC694
            .unwrap()
ef7d6dee5976atagen2026-10-10 13:23 UTC695
            .contains("Someone is at the door.")
ef7d6dee5976atagen2026-10-10 13:23 UTC696
    );
ef7d6dee5976atagen2026-10-10 13:23 UTC697
    let excuse = [
ef7d6dee5976atagen2026-10-10 13:23 UTC698
        ("accept-language", "en-US"),
ef7d6dee5976atagen2026-10-10 13:23 UTC699
        (
ef7d6dee5976atagen2026-10-10 13:23 UTC700
            "authorization",
ef7d6dee5976atagen2026-10-10 13:23 UTC701
            "Digest username=\"carving the turkey\", nonce=\"abc\"",
ef7d6dee5976atagen2026-10-10 13:23 UTC702
        ),
ef7d6dee5976atagen2026-10-10 13:23 UTC703
    ];
ef7d6dee5976atagen2026-10-10 13:23 UTC704
    let answered = send(&app, "GET", "/runs/cafebabe", &excuse, "").await;
ef7d6dee5976atagen2026-10-10 13:23 UTC705
    assert_eq!(answered.status(), StatusCode::OK);
ef7d6dee5976atagen2026-10-10 13:23 UTC706
    let floor = json(send(&app, "GET", "/runs/cafebabe/floor", &us, "").await).await;
ef7d6dee5976atagen2026-10-10 13:23 UTC707
    assert_eq!(floor["name"], "Grandma's house for Thanksgiving");
ef7d6dee5976atagen2026-10-10 13:23 UTC708
}
ef7d6dee5976atagen2026-10-10 13:23 UTC709
ef7d6dee5976atagen2026-10-10 13:23 UTC710
#[tokio::test]
ef7d6dee5976atagen2026-10-10 13:23 UTC711
async fn the_default_locale_is_british() {
ef7d6dee5976atagen2026-10-10 13:23 UTC712
    let (app, _) = seeded(Floor::Nans);
ef7d6dee5976atagen2026-10-10 13:23 UTC713
    let floor = send(&app, "GET", "/runs/cafebabe/floor", &[], "").await;
ef7d6dee5976atagen2026-10-10 13:23 UTC714
    assert_eq!(header(&floor, "content-language"), "en-GB");
ef7d6dee5976atagen2026-10-10 13:23 UTC715
    assert!(header(&floor, "vary").contains("Accept-Language"));
ef7d6dee5976atagen2026-10-10 13:23 UTC716
    assert_eq!(json(floor).await["name"], "Nan's house at Christmas");
ef7d6dee5976atagen2026-10-10 13:23 UTC717
}
58e1fb3943e6atagen2026-10-10 13:30 UTC718
58e1fb3943e6atagen2026-10-10 13:30 UTC719
fn advertised(response: &Response) -> Vec<String> {
58e1fb3943e6atagen2026-10-10 13:30 UTC720
    header(response, "link")
58e1fb3943e6atagen2026-10-10 13:30 UTC721
        .split(", <")
58e1fb3943e6atagen2026-10-10 13:30 UTC722
        .filter(|entry| entry.contains("rels/technique"))
58e1fb3943e6atagen2026-10-10 13:30 UTC723
        .filter_map(|entry| {
58e1fb3943e6atagen2026-10-10 13:30 UTC724
            entry
58e1fb3943e6atagen2026-10-10 13:30 UTC725
                .split("title=\"")
58e1fb3943e6atagen2026-10-10 13:30 UTC726
                .nth(1)?
58e1fb3943e6atagen2026-10-10 13:30 UTC727
                .split('"')
58e1fb3943e6atagen2026-10-10 13:30 UTC728
                .next()
58e1fb3943e6atagen2026-10-10 13:30 UTC729
                .map(str::to_owned)
58e1fb3943e6atagen2026-10-10 13:30 UTC730
        })
58e1fb3943e6atagen2026-10-10 13:30 UTC731
        .collect()
58e1fb3943e6atagen2026-10-10 13:30 UTC732
}
58e1fb3943e6atagen2026-10-10 13:30 UTC733
58e1fb3943e6atagen2026-10-10 13:30 UTC734
#[tokio::test]
58e1fb3943e6atagen2026-10-10 13:30 UTC735
async fn links_offer_only_usable_techniques() {
58e1fb3943e6atagen2026-10-10 13:30 UTC736
    let app = seeded_with(Floor::Bedroom, |run| run.foreskin = Some(1.0));
58e1fb3943e6atagen2026-10-10 13:30 UTC737
    let cold = send(&app, "GET", "/runs/cafebabe", &[], "").await;
58e1fb3943e6atagen2026-10-10 13:30 UTC738
    assert_eq!(advertised(&cold), ["warmup", "ice"]);
58e1fb3943e6atagen2026-10-10 13:30 UTC739
    let warm = send(&app, "JELQ", "/runs/cafebabe?technique=warmup", &[], "").await;
58e1fb3943e6atagen2026-10-10 13:30 UTC740
    let usable = advertised(&warm);
58e1fb3943e6atagen2026-10-10 13:30 UTC741
    assert!(usable.contains(&"wet".to_owned()) && usable.contains(&"dry".to_owned()));
58e1fb3943e6atagen2026-10-10 13:30 UTC742
    assert!(!usable.contains(&"uli".to_owned()), "uli needs the forum");
58e1fb3943e6atagen2026-10-10 13:30 UTC743
    assert!(!usable.contains(&"pump".to_owned()), "pump needs a pump");
58e1fb3943e6atagen2026-10-10 13:30 UTC744
    assert!(
58e1fb3943e6atagen2026-10-10 13:30 UTC745
        header(&warm, "link").contains("title=\"wet\"; allow=\"JELQ\"; lubricant=\"required\"")
58e1fb3943e6atagen2026-10-10 13:30 UTC746
    );
58e1fb3943e6atagen2026-10-10 13:30 UTC747
58e1fb3943e6atagen2026-10-10 13:30 UTC748
    let (forum, _) = seeded(Floor::Forum);
58e1fb3943e6atagen2026-10-10 13:30 UTC749
    let response = send(&forum, "GET", "/runs/cafebabe", &[], "").await;
58e1fb3943e6atagen2026-10-10 13:30 UTC750
    assert!(advertised(&response).is_empty());
58e1fb3943e6atagen2026-10-10 13:30 UTC751
}