atagen

jelq

public

an http based roguelike rpg about jelqing

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

Blame: tests/http.rs

a9b016e4b248atagen2026-10-10 08:12 UTC1
use std::sync::Arc;
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},
a9b016e4b248atagen2026-10-10 08:12 UTC6
    http::{Method, Request, StatusCode, header},
a9b016e4b248atagen2026-10-10 08:12 UTC7
    response::Response,
a9b016e4b248atagen2026-10-10 08:12 UTC8
};
dfa38cf4cd4batagen2026-10-10 09:17 UTC9
use jelqd::{
dfa38cf4cd4batagen2026-10-10 09:17 UTC10
    AppState,
dfa38cf4cd4batagen2026-10-10 09:17 UTC11
    game::{Floor, Run},
dfa38cf4cd4batagen2026-10-10 09:17 UTC12
    router,
dfa38cf4cd4batagen2026-10-10 09:17 UTC13
};
a9b016e4b248atagen2026-10-10 08:12 UTC14
use serde_json::Value;
a9b016e4b248atagen2026-10-10 08:12 UTC15
use tower::ServiceExt;
a9b016e4b248atagen2026-10-10 08:12 UTC16
a9b016e4b248atagen2026-10-10 08:12 UTC17
async fn send(
a9b016e4b248atagen2026-10-10 08:12 UTC18
    app: &Router,
a9b016e4b248atagen2026-10-10 08:12 UTC19
    method: &str,
a9b016e4b248atagen2026-10-10 08:12 UTC20
    uri: &str,
a9b016e4b248atagen2026-10-10 08:12 UTC21
    headers: &[(&str, &str)],
a9b016e4b248atagen2026-10-10 08:12 UTC22
    body: &str,
a9b016e4b248atagen2026-10-10 08:12 UTC23
) -> Response {
a9b016e4b248atagen2026-10-10 08:12 UTC24
    let mut request = Request::builder()
a9b016e4b248atagen2026-10-10 08:12 UTC25
        .method(Method::from_bytes(method.as_bytes()).unwrap())
a9b016e4b248atagen2026-10-10 08:12 UTC26
        .uri(uri);
a9b016e4b248atagen2026-10-10 08:12 UTC27
    for (name, value) in headers {
a9b016e4b248atagen2026-10-10 08:12 UTC28
        request = request.header(*name, *value);
a9b016e4b248atagen2026-10-10 08:12 UTC29
    }
a9b016e4b248atagen2026-10-10 08:12 UTC30
    app.clone()
a9b016e4b248atagen2026-10-10 08:12 UTC31
        .oneshot(request.body(Body::from(body.to_owned())).unwrap())
a9b016e4b248atagen2026-10-10 08:12 UTC32
        .await
a9b016e4b248atagen2026-10-10 08:12 UTC33
        .unwrap()
a9b016e4b248atagen2026-10-10 08:12 UTC34
}
a9b016e4b248atagen2026-10-10 08:12 UTC35
a9b016e4b248atagen2026-10-10 08:12 UTC36
async fn json(response: Response) -> Value {
a9b016e4b248atagen2026-10-10 08:12 UTC37
    serde_json::from_slice(&to_bytes(response.into_body(), usize::MAX).await.unwrap()).unwrap()
a9b016e4b248atagen2026-10-10 08:12 UTC38
}
a9b016e4b248atagen2026-10-10 08:12 UTC39
dfa38cf4cd4batagen2026-10-10 09:17 UTC40
fn seeded(floor: Floor) -> (Router, String) {
a9b016e4b248atagen2026-10-10 08:12 UTC41
    let state = Arc::new(AppState::default());
a9b016e4b248atagen2026-10-10 08:12 UTC42
    let mut run = Run::new("cafebabe".into(), 69, 0);
dfa38cf4cd4batagen2026-10-10 09:17 UTC43
    run.floor = floor;
a9b016e4b248atagen2026-10-10 08:12 UTC44
    let etag = run.origin_etag.clone();
a9b016e4b248atagen2026-10-10 08:12 UTC45
    state.insert(run);
a9b016e4b248atagen2026-10-10 08:12 UTC46
    (router(state), etag)
a9b016e4b248atagen2026-10-10 08:12 UTC47
}
a9b016e4b248atagen2026-10-10 08:12 UTC48
a9b016e4b248atagen2026-10-10 08:12 UTC49
#[tokio::test]
a9b016e4b248atagen2026-10-10 08:12 UTC50
async fn creating_a_run_returns_location_and_etag() {
a9b016e4b248atagen2026-10-10 08:12 UTC51
    let app = jelqd::app();
a9b016e4b248atagen2026-10-10 08:12 UTC52
    let response = send(&app, "POST", "/runs?seed=1", &[], "").await;
a9b016e4b248atagen2026-10-10 08:12 UTC53
    assert_eq!(response.status(), StatusCode::CREATED);
a9b016e4b248atagen2026-10-10 08:12 UTC54
    assert!(response.headers().contains_key(header::LOCATION));
a9b016e4b248atagen2026-10-10 08:12 UTC55
    assert!(response.headers().contains_key(header::ETAG));
a9b016e4b248atagen2026-10-10 08:12 UTC56
    assert_eq!(response.headers()[header::SERVER], "jelqd/0.6.9");
a9b016e4b248atagen2026-10-10 08:12 UTC57
}
a9b016e4b248atagen2026-10-10 08:12 UTC58
a9b016e4b248atagen2026-10-10 08:12 UTC59
#[tokio::test]
a9b016e4b248atagen2026-10-10 08:12 UTC60
async fn jelqing_cold_fails_the_precondition() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC61
    let (app, _) = seeded(Floor::Bedroom);
a9b016e4b248atagen2026-10-10 08:12 UTC62
    let response = send(&app, "JELQ", "/runs/cafebabe?technique=stretch", &[], "").await;
a9b016e4b248atagen2026-10-10 08:12 UTC63
    assert_eq!(response.status(), StatusCode::PRECONDITION_FAILED);
a9b016e4b248atagen2026-10-10 08:12 UTC64
    assert_eq!(
a9b016e4b248atagen2026-10-10 08:12 UTC65
        response.headers()[header::CONTENT_TYPE],
a9b016e4b248atagen2026-10-10 08:12 UTC66
        "application/problem+json"
a9b016e4b248atagen2026-10-10 08:12 UTC67
    );
a9b016e4b248atagen2026-10-10 08:12 UTC68
}
a9b016e4b248atagen2026-10-10 08:12 UTC69
a9b016e4b248atagen2026-10-10 08:12 UTC70
#[tokio::test]
a9b016e4b248atagen2026-10-10 08:12 UTC71
async fn wet_without_lube_is_unsupported_media() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC72
    let (app, _) = seeded(Floor::Bedroom);
a9b016e4b248atagen2026-10-10 08:12 UTC73
    send(&app, "JELQ", "/runs/cafebabe?technique=warmup", &[], "").await;
a9b016e4b248atagen2026-10-10 08:12 UTC74
    let response = send(&app, "JELQ", "/runs/cafebabe?technique=wet", &[], "").await;
a9b016e4b248atagen2026-10-10 08:12 UTC75
    assert_eq!(response.status(), StatusCode::UNSUPPORTED_MEDIA_TYPE);
a9b016e4b248atagen2026-10-10 08:12 UTC76
    let images = send(
a9b016e4b248atagen2026-10-10 08:12 UTC77
        &app,
a9b016e4b248atagen2026-10-10 08:12 UTC78
        "JELQ",
a9b016e4b248atagen2026-10-10 08:12 UTC79
        "/runs/cafebabe?technique=wet",
a9b016e4b248atagen2026-10-10 08:12 UTC80
        &[("content-type", "image/jpeg")],
a9b016e4b248atagen2026-10-10 08:12 UTC81
        "",
a9b016e4b248atagen2026-10-10 08:12 UTC82
    )
a9b016e4b248atagen2026-10-10 08:12 UTC83
    .await;
a9b016e4b248atagen2026-10-10 08:12 UTC84
    assert_eq!(json(images).await["detail"], "No.");
a9b016e4b248atagen2026-10-10 08:12 UTC85
}
a9b016e4b248atagen2026-10-10 08:12 UTC86
a9b016e4b248atagen2026-10-10 08:12 UTC87
#[tokio::test]
a9b016e4b248atagen2026-10-10 08:12 UTC88
async fn unknown_methods_are_not_implemented() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC89
    let (app, _) = seeded(Floor::Bedroom);
a9b016e4b248atagen2026-10-10 08:12 UTC90
    let response = send(&app, "MILK", "/runs/cafebabe", &[], "").await;
a9b016e4b248atagen2026-10-10 08:12 UTC91
    assert_eq!(response.status(), StatusCode::NOT_IMPLEMENTED);
a9b016e4b248atagen2026-10-10 08:12 UTC92
}
a9b016e4b248atagen2026-10-10 08:12 UTC93
a9b016e4b248atagen2026-10-10 08:12 UTC94
#[tokio::test]
a9b016e4b248atagen2026-10-10 08:12 UTC95
async fn patch_only_accepts_the_truth() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC96
    let (app, _) = seeded(Floor::Bedroom);
a9b016e4b248atagen2026-10-10 08:12 UTC97
    let patch = [("content-type", "application/json-patch+json")];
a9b016e4b248atagen2026-10-10 08:12 UTC98
    let replace = send(
a9b016e4b248atagen2026-10-10 08:12 UTC99
        &app,
a9b016e4b248atagen2026-10-10 08:12 UTC100
        "PATCH",
a9b016e4b248atagen2026-10-10 08:12 UTC101
        "/runs/cafebabe",
a9b016e4b248atagen2026-10-10 08:12 UTC102
        &patch,
a9b016e4b248atagen2026-10-10 08:12 UTC103
        r#"[{"op":"replace","path":"/length","value":12}]"#,
a9b016e4b248atagen2026-10-10 08:12 UTC104
    )
a9b016e4b248atagen2026-10-10 08:12 UTC105
    .await;
a9b016e4b248atagen2026-10-10 08:12 UTC106
    assert_eq!(replace.status(), StatusCode::UNPROCESSABLE_ENTITY);
a9b016e4b248atagen2026-10-10 08:12 UTC107
    let lie = send(
a9b016e4b248atagen2026-10-10 08:12 UTC108
        &app,
a9b016e4b248atagen2026-10-10 08:12 UTC109
        "PATCH",
a9b016e4b248atagen2026-10-10 08:12 UTC110
        "/runs/cafebabe",
a9b016e4b248atagen2026-10-10 08:12 UTC111
        &patch,
a9b016e4b248atagen2026-10-10 08:12 UTC112
        r#"[{"op":"test","path":"/length","value":40}]"#,
a9b016e4b248atagen2026-10-10 08:12 UTC113
    )
a9b016e4b248atagen2026-10-10 08:12 UTC114
    .await;
a9b016e4b248atagen2026-10-10 08:12 UTC115
    assert_eq!(
a9b016e4b248atagen2026-10-10 08:12 UTC116
        json(lie).await["type"],
a9b016e4b248atagen2026-10-10 08:12 UTC117
        "https://jelq.dev/problems/exaggeration"
a9b016e4b248atagen2026-10-10 08:12 UTC118
    );
a9b016e4b248atagen2026-10-10 08:12 UTC119
}
a9b016e4b248atagen2026-10-10 08:12 UTC120
a9b016e4b248atagen2026-10-10 08:12 UTC121
#[tokio::test]
a9b016e4b248atagen2026-10-10 08:12 UTC122
async fn deleting_the_foreskin_requires_certainty() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC123
    let (app, _) = seeded(Floor::Bedroom);
a9b016e4b248atagen2026-10-10 08:12 UTC124
    let response = send(&app, "DELETE", "/runs/cafebabe/foreskin", &[], "").await;
a9b016e4b248atagen2026-10-10 08:12 UTC125
    assert_eq!(response.status(), StatusCode::PRECONDITION_REQUIRED);
a9b016e4b248atagen2026-10-10 08:12 UTC126
    let etag = send(&app, "GET", "/runs/cafebabe/foreskin", &[], "")
a9b016e4b248atagen2026-10-10 08:12 UTC127
        .await
a9b016e4b248atagen2026-10-10 08:12 UTC128
        .headers()[header::ETAG]
a9b016e4b248atagen2026-10-10 08:12 UTC129
        .to_str()
a9b016e4b248atagen2026-10-10 08:12 UTC130
        .unwrap()
a9b016e4b248atagen2026-10-10 08:12 UTC131
        .to_owned();
a9b016e4b248atagen2026-10-10 08:12 UTC132
    let response = send(
a9b016e4b248atagen2026-10-10 08:12 UTC133
        &app,
a9b016e4b248atagen2026-10-10 08:12 UTC134
        "DELETE",
a9b016e4b248atagen2026-10-10 08:12 UTC135
        "/runs/cafebabe/foreskin",
a9b016e4b248atagen2026-10-10 08:12 UTC136
        &[("if-match", &etag)],
a9b016e4b248atagen2026-10-10 08:12 UTC137
        "",
a9b016e4b248atagen2026-10-10 08:12 UTC138
    )
a9b016e4b248atagen2026-10-10 08:12 UTC139
    .await;
a9b016e4b248atagen2026-10-10 08:12 UTC140
    assert_eq!(response.status(), StatusCode::NO_CONTENT);
a9b016e4b248atagen2026-10-10 08:12 UTC141
    let response = send(&app, "GET", "/runs/cafebabe/foreskin", &[], "").await;
a9b016e4b248atagen2026-10-10 08:12 UTC142
    assert_eq!(response.status(), StatusCode::GONE);
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 length_is_chunked_until_the_ruler() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC147
    let (app, _) = seeded(Floor::Bedroom);
a9b016e4b248atagen2026-10-10 08:12 UTC148
    let lie = send(&app, "GET", "/runs/cafebabe/length", &[], "").await;
a9b016e4b248atagen2026-10-10 08:12 UTC149
    assert!(!lie.headers().contains_key(header::CONTENT_LENGTH));
a9b016e4b248atagen2026-10-10 08:12 UTC150
    let found = send(&app, "POST", "/runs/cafebabe/drawer", &[], "").await;
a9b016e4b248atagen2026-10-10 08:12 UTC151
    assert_eq!(found.status(), StatusCode::FOUND);
a9b016e4b248atagen2026-10-10 08:12 UTC152
    let truth = send(&app, "GET", "/runs/cafebabe/length", &[], "").await;
a9b016e4b248atagen2026-10-10 08:12 UTC153
    let declared: usize = truth.headers()[header::CONTENT_LENGTH]
a9b016e4b248atagen2026-10-10 08:12 UTC154
        .to_str()
a9b016e4b248atagen2026-10-10 08:12 UTC155
        .unwrap()
a9b016e4b248atagen2026-10-10 08:12 UTC156
        .parse()
a9b016e4b248atagen2026-10-10 08:12 UTC157
        .unwrap();
a9b016e4b248atagen2026-10-10 08:12 UTC158
    let body = to_bytes(truth.into_body(), usize::MAX).await.unwrap();
a9b016e4b248atagen2026-10-10 08:12 UTC159
    assert_eq!(body.len(), declared);
a9b016e4b248atagen2026-10-10 08:12 UTC160
    assert!(body.starts_with(b"8=") && body.ends_with(b"D"));
a9b016e4b248atagen2026-10-10 08:12 UTC161
}
a9b016e4b248atagen2026-10-10 08:12 UTC162
a9b016e4b248atagen2026-10-10 08:12 UTC163
#[tokio::test]
a9b016e4b248atagen2026-10-10 08:12 UTC164
async fn the_clinic_escalates_toward_honesty() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC165
    let (app, _) = seeded(Floor::Clinic);
a9b016e4b248atagen2026-10-10 08:12 UTC166
    let mut hints = Vec::new();
a9b016e4b248atagen2026-10-10 08:12 UTC167
    for _ in 0..3 {
a9b016e4b248atagen2026-10-10 08:12 UTC168
        let response = send(
a9b016e4b248atagen2026-10-10 08:12 UTC169
            &app,
a9b016e4b248atagen2026-10-10 08:12 UTC170
            "GET",
a9b016e4b248atagen2026-10-10 08:12 UTC171
            "/runs/cafebabe/diagnosis",
a9b016e4b248atagen2026-10-10 08:12 UTC172
            &[("authorization", "Excuse \"changing\"")],
a9b016e4b248atagen2026-10-10 08:12 UTC173
            "",
a9b016e4b248atagen2026-10-10 08:12 UTC174
        )
a9b016e4b248atagen2026-10-10 08:12 UTC175
        .await;
a9b016e4b248atagen2026-10-10 08:12 UTC176
        assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
a9b016e4b248atagen2026-10-10 08:12 UTC177
        hints.push(
a9b016e4b248atagen2026-10-10 08:12 UTC178
            response.headers()[header::WWW_AUTHENTICATE]
a9b016e4b248atagen2026-10-10 08:12 UTC179
                .to_str()
a9b016e4b248atagen2026-10-10 08:12 UTC180
                .unwrap()
a9b016e4b248atagen2026-10-10 08:12 UTC181
                .to_owned(),
a9b016e4b248atagen2026-10-10 08:12 UTC182
        );
a9b016e4b248atagen2026-10-10 08:12 UTC183
    }
a9b016e4b248atagen2026-10-10 08:12 UTC184
    assert!(hints[2].contains("Be honest."));
a9b016e4b248atagen2026-10-10 08:12 UTC185
}
a9b016e4b248atagen2026-10-10 08:12 UTC186
a9b016e4b248atagen2026-10-10 08:12 UTC187
#[tokio::test]
a9b016e4b248atagen2026-10-10 08:12 UTC188
async fn the_true_ending_is_not_modified() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC189
    let (app, etag) = seeded(Floor::Clinic);
dfa38cf4cd4batagen2026-10-10 09:17 UTC190
    let early = seeded(Floor::Bedroom).0;
a9b016e4b248atagen2026-10-10 08:12 UTC191
    let response = send(&early, "GET", "/runs/cafebabe/diagnosis", &[], "").await;
a9b016e4b248atagen2026-10-10 08:12 UTC192
    assert_eq!(response.status(), StatusCode::TOO_EARLY);
a9b016e4b248atagen2026-10-10 08:12 UTC193
a9b016e4b248atagen2026-10-10 08:12 UTC194
    let response = send(
a9b016e4b248atagen2026-10-10 08:12 UTC195
        &app,
a9b016e4b248atagen2026-10-10 08:12 UTC196
        "GET",
a9b016e4b248atagen2026-10-10 08:12 UTC197
        "/runs/cafebabe/diagnosis",
a9b016e4b248atagen2026-10-10 08:12 UTC198
        &[
a9b016e4b248atagen2026-10-10 08:12 UTC199
            ("authorization", "Bearer honesty"),
a9b016e4b248atagen2026-10-10 08:12 UTC200
            ("if-none-match", &etag),
a9b016e4b248atagen2026-10-10 08:12 UTC201
        ],
a9b016e4b248atagen2026-10-10 08:12 UTC202
        "",
a9b016e4b248atagen2026-10-10 08:12 UTC203
    )
a9b016e4b248atagen2026-10-10 08:12 UTC204
    .await;
a9b016e4b248atagen2026-10-10 08:12 UTC205
    assert_eq!(response.status(), StatusCode::NOT_MODIFIED);
a9b016e4b248atagen2026-10-10 08:12 UTC206
    assert_eq!(response.headers()["x-diagnosis"], "normal");
a9b016e4b248atagen2026-10-10 08:12 UTC207
}
a9b016e4b248atagen2026-10-10 08:12 UTC208
a9b016e4b248atagen2026-10-10 08:12 UTC209
#[tokio::test]
a9b016e4b248atagen2026-10-10 08:12 UTC210
async fn mom_accepts_a_known_excuse() {
a9b016e4b248atagen2026-10-10 08:12 UTC211
    let state = Arc::new(AppState::default());
a9b016e4b248atagen2026-10-10 08:12 UTC212
    let mut run = Run::new("cafebabe".into(), 69, 0);
a9b016e4b248atagen2026-10-10 08:12 UTC213
    run.mom_present = true;
a9b016e4b248atagen2026-10-10 08:12 UTC214
    state.insert(run);
a9b016e4b248atagen2026-10-10 08:12 UTC215
    let app = router(state);
a9b016e4b248atagen2026-10-10 08:12 UTC216
    let response = send(&app, "GET", "/runs/cafebabe", &[], "").await;
a9b016e4b248atagen2026-10-10 08:12 UTC217
    assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
a9b016e4b248atagen2026-10-10 08:12 UTC218
    assert_eq!(response.headers()["via"], "1.1 mom");
a9b016e4b248atagen2026-10-10 08:12 UTC219
    let response = send(
a9b016e4b248atagen2026-10-10 08:12 UTC220
        &app,
a9b016e4b248atagen2026-10-10 08:12 UTC221
        "GET",
a9b016e4b248atagen2026-10-10 08:12 UTC222
        "/runs/cafebabe",
a9b016e4b248atagen2026-10-10 08:12 UTC223
        &[("authorization", "Excuse \"homework\"")],
a9b016e4b248atagen2026-10-10 08:12 UTC224
        "",
a9b016e4b248atagen2026-10-10 08:12 UTC225
    )
a9b016e4b248atagen2026-10-10 08:12 UTC226
    .await;
a9b016e4b248atagen2026-10-10 08:12 UTC227
    assert_eq!(response.status(), StatusCode::OK);
a9b016e4b248atagen2026-10-10 08:12 UTC228
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC229
dfa38cf4cd4batagen2026-10-10 09:17 UTC230
fn seeded_with(floor: Floor, setup: impl FnOnce(&mut Run)) -> Router {
dfa38cf4cd4batagen2026-10-10 09:17 UTC231
    let state = Arc::new(AppState::default());
dfa38cf4cd4batagen2026-10-10 09:17 UTC232
    let mut run = Run::new("cafebabe".into(), 69, 0);
dfa38cf4cd4batagen2026-10-10 09:17 UTC233
    run.floor = floor;
dfa38cf4cd4batagen2026-10-10 09:17 UTC234
    setup(&mut run);
dfa38cf4cd4batagen2026-10-10 09:17 UTC235
    state.insert(run);
dfa38cf4cd4batagen2026-10-10 09:17 UTC236
    router(state)
dfa38cf4cd4batagen2026-10-10 09:17 UTC237
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC238
dfa38cf4cd4batagen2026-10-10 09:17 UTC239
#[tokio::test]
dfa38cf4cd4batagen2026-10-10 09:17 UTC240
async fn floors_end_when_they_end() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC241
    let app = seeded_with(Floor::Bedroom, |_| {});
dfa38cf4cd4batagen2026-10-10 09:17 UTC242
    let early = send(&app, "POST", "/runs/cafebabe/floor", &[], "").await;
dfa38cf4cd4batagen2026-10-10 09:17 UTC243
    assert_eq!(early.status(), StatusCode::TOO_EARLY);
dfa38cf4cd4batagen2026-10-10 09:17 UTC244
    let app = seeded_with(Floor::Bedroom, |run| run.floor_turns = 6);
dfa38cf4cd4batagen2026-10-10 09:17 UTC245
    let next = send(&app, "POST", "/runs/cafebabe/floor", &[], "").await;
dfa38cf4cd4batagen2026-10-10 09:17 UTC246
    assert_eq!(next.status(), StatusCode::SEE_OTHER);
dfa38cf4cd4batagen2026-10-10 09:17 UTC247
    assert_eq!(next.headers()["x-floor"], "2");
dfa38cf4cd4batagen2026-10-10 09:17 UTC248
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC249
dfa38cf4cd4batagen2026-10-10 09:17 UTC250
#[tokio::test]
dfa38cf4cd4batagen2026-10-10 09:17 UTC251
async fn pump_bro_upsells() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC252
    let closed = seeded_with(Floor::Bedroom, |_| {});
dfa38cf4cd4batagen2026-10-10 09:17 UTC253
    let response = send(&closed, "GET", "/runs/cafebabe/shop", &[], "").await;
dfa38cf4cd4batagen2026-10-10 09:17 UTC254
    assert_eq!(response.status(), StatusCode::NOT_FOUND);
dfa38cf4cd4batagen2026-10-10 09:17 UTC255
dfa38cf4cd4batagen2026-10-10 09:17 UTC256
    let app = seeded_with(Floor::LockerRoom, |_| {});
dfa38cf4cd4batagen2026-10-10 09:17 UTC257
    let pump = send(&app, "POST", "/runs/cafebabe/shop/pump", &[], "").await;
dfa38cf4cd4batagen2026-10-10 09:17 UTC258
    assert_eq!(pump.status(), StatusCode::UPGRADE_REQUIRED);
dfa38cf4cd4batagen2026-10-10 09:17 UTC259
    assert_eq!(pump.headers()[header::UPGRADE], "pump-pro/2.0");
dfa38cf4cd4batagen2026-10-10 09:17 UTC260
    let pro = send(&app, "POST", "/runs/cafebabe/shop/pump-pro", &[], "").await;
dfa38cf4cd4batagen2026-10-10 09:17 UTC261
    assert_eq!(pro.status(), StatusCode::CREATED);
dfa38cf4cd4batagen2026-10-10 09:17 UTC262
    let broke = send(&app, "POST", "/runs/cafebabe/shop/extender", &[], "").await;
dfa38cf4cd4batagen2026-10-10 09:17 UTC263
    assert_eq!(broke.status(), StatusCode::PAYMENT_REQUIRED);
dfa38cf4cd4batagen2026-10-10 09:17 UTC264
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC265
dfa38cf4cd4batagen2026-10-10 09:17 UTC266
#[tokio::test]
dfa38cf4cd4batagen2026-10-10 09:17 UTC267
async fn combos_are_multi_status_xml() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC268
    let app = seeded_with(Floor::Bedroom, |run| run.foreskin = Some(1.0));
dfa38cf4cd4batagen2026-10-10 09:17 UTC269
    send(&app, "JELQ", "/runs/cafebabe?technique=warmup", &[], "").await;
dfa38cf4cd4batagen2026-10-10 09:17 UTC270
    let response = send(
dfa38cf4cd4batagen2026-10-10 09:17 UTC271
        &app,
dfa38cf4cd4batagen2026-10-10 09:17 UTC272
        "JELQ",
dfa38cf4cd4batagen2026-10-10 09:17 UTC273
        "/runs/cafebabe?technique=wet,stretch",
dfa38cf4cd4batagen2026-10-10 09:17 UTC274
        &[("content-type", "application/baby-oil")],
dfa38cf4cd4batagen2026-10-10 09:17 UTC275
        "",
dfa38cf4cd4batagen2026-10-10 09:17 UTC276
    )
dfa38cf4cd4batagen2026-10-10 09:17 UTC277
    .await;
dfa38cf4cd4batagen2026-10-10 09:17 UTC278
    assert_eq!(response.status(), StatusCode::MULTI_STATUS);
dfa38cf4cd4batagen2026-10-10 09:17 UTC279
    let body = to_bytes(response.into_body(), usize::MAX).await.unwrap();
dfa38cf4cd4batagen2026-10-10 09:17 UTC280
    assert!(String::from_utf8_lossy(&body).contains("<D:multistatus"));
dfa38cf4cd4batagen2026-10-10 09:17 UTC281
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC282
dfa38cf4cd4batagen2026-10-10 09:17 UTC283
#[tokio::test]
dfa38cf4cd4batagen2026-10-10 09:17 UTC284
async fn the_elder_is_stale() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC285
    let app = seeded_with(Floor::Forum, |run| run.length = 5.0);
dfa38cf4cd4batagen2026-10-10 09:17 UTC286
    let unverified = send(&app, "GET", "/runs/cafebabe/elder", &[], "").await;
dfa38cf4cd4batagen2026-10-10 09:17 UTC287
    assert_eq!(
dfa38cf4cd4batagen2026-10-10 09:17 UTC288
        unverified.status(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC289
        StatusCode::PROXY_AUTHENTICATION_REQUIRED
dfa38cf4cd4batagen2026-10-10 09:17 UTC290
    );
dfa38cf4cd4batagen2026-10-10 09:17 UTC291
    let lie = send(
dfa38cf4cd4batagen2026-10-10 09:17 UTC292
        &app,
dfa38cf4cd4batagen2026-10-10 09:17 UTC293
        "GET",
dfa38cf4cd4batagen2026-10-10 09:17 UTC294
        "/runs/cafebabe/elder",
dfa38cf4cd4batagen2026-10-10 09:17 UTC295
        &[("proxy-authorization", "Ruler 9")],
dfa38cf4cd4batagen2026-10-10 09:17 UTC296
        "",
dfa38cf4cd4batagen2026-10-10 09:17 UTC297
    )
dfa38cf4cd4batagen2026-10-10 09:17 UTC298
    .await;
dfa38cf4cd4batagen2026-10-10 09:17 UTC299
    assert_eq!(lie.status(), StatusCode::BAD_REQUEST);
dfa38cf4cd4batagen2026-10-10 09:17 UTC300
    let csv = send(
dfa38cf4cd4batagen2026-10-10 09:17 UTC301
        &app,
dfa38cf4cd4batagen2026-10-10 09:17 UTC302
        "GET",
dfa38cf4cd4batagen2026-10-10 09:17 UTC303
        "/runs/cafebabe/elder",
dfa38cf4cd4batagen2026-10-10 09:17 UTC304
        &[("proxy-authorization", "Ruler 5.0")],
dfa38cf4cd4batagen2026-10-10 09:17 UTC305
        "",
dfa38cf4cd4batagen2026-10-10 09:17 UTC306
    )
dfa38cf4cd4batagen2026-10-10 09:17 UTC307
    .await;
dfa38cf4cd4batagen2026-10-10 09:17 UTC308
    assert_eq!(csv.status(), StatusCode::OK);
dfa38cf4cd4batagen2026-10-10 09:17 UTC309
    let stale = send(
dfa38cf4cd4batagen2026-10-10 09:17 UTC310
        &app,
dfa38cf4cd4batagen2026-10-10 09:17 UTC311
        "GET",
dfa38cf4cd4batagen2026-10-10 09:17 UTC312
        "/runs/cafebabe/elder",
dfa38cf4cd4batagen2026-10-10 09:17 UTC313
        &[("if-modified-since", "Mon, 15 Jun 2009 00:00:00 GMT")],
dfa38cf4cd4batagen2026-10-10 09:17 UTC314
        "",
dfa38cf4cd4batagen2026-10-10 09:17 UTC315
    )
dfa38cf4cd4batagen2026-10-10 09:17 UTC316
    .await;
dfa38cf4cd4batagen2026-10-10 09:17 UTC317
    assert_eq!(stale.status(), StatusCode::NOT_MODIFIED);
dfa38cf4cd4batagen2026-10-10 09:17 UTC318
    let exit = send(&app, "POST", "/runs/cafebabe/floor", &[], "").await;
dfa38cf4cd4batagen2026-10-10 09:17 UTC319
    assert_eq!(exit.status(), StatusCode::SEE_OTHER);
dfa38cf4cd4batagen2026-10-10 09:17 UTC320
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC321
dfa38cf4cd4batagen2026-10-10 09:17 UTC322
#[tokio::test]
dfa38cf4cd4batagen2026-10-10 09:17 UTC323
async fn the_hotel_has_a_captive_portal() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC324
    let app = seeded_with(Floor::Hotel, |_| {});
dfa38cf4cd4batagen2026-10-10 09:17 UTC325
    let blocked = send(&app, "GET", "/runs/cafebabe", &[], "").await;
dfa38cf4cd4batagen2026-10-10 09:17 UTC326
    assert_eq!(
dfa38cf4cd4batagen2026-10-10 09:17 UTC327
        blocked.status(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC328
        StatusCode::NETWORK_AUTHENTICATION_REQUIRED
dfa38cf4cd4batagen2026-10-10 09:17 UTC329
    );
dfa38cf4cd4batagen2026-10-10 09:17 UTC330
    let accepted = send(&app, "POST", "/runs/cafebabe/portal", &[], "").await;
dfa38cf4cd4batagen2026-10-10 09:17 UTC331
    assert_eq!(accepted.status(), StatusCode::SEE_OTHER);
dfa38cf4cd4batagen2026-10-10 09:17 UTC332
    let online = send(&app, "GET", "/runs/cafebabe", &[], "").await;
dfa38cf4cd4batagen2026-10-10 09:17 UTC333
    assert_eq!(online.status(), StatusCode::OK);
dfa38cf4cd4batagen2026-10-10 09:17 UTC334
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC335
dfa38cf4cd4batagen2026-10-10 09:17 UTC336
#[tokio::test]
dfa38cf4cd4batagen2026-10-10 09:17 UTC337
async fn mom_remembers_excuses_by_thanksgiving() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC338
    let app = seeded_with(Floor::Grandmas, |run| {
dfa38cf4cd4batagen2026-10-10 09:17 UTC339
        run.mom_present = true;
dfa38cf4cd4batagen2026-10-10 09:17 UTC340
        run.nonce = "abc".into();
dfa38cf4cd4batagen2026-10-10 09:17 UTC341
        run.heard.push("homework".into());
dfa38cf4cd4batagen2026-10-10 09:17 UTC342
    });
dfa38cf4cd4batagen2026-10-10 09:17 UTC343
    let basic = send(
dfa38cf4cd4batagen2026-10-10 09:17 UTC344
        &app,
dfa38cf4cd4batagen2026-10-10 09:17 UTC345
        "GET",
dfa38cf4cd4batagen2026-10-10 09:17 UTC346
        "/runs/cafebabe",
dfa38cf4cd4batagen2026-10-10 09:17 UTC347
        &[("authorization", "Excuse \"changing\"")],
dfa38cf4cd4batagen2026-10-10 09:17 UTC348
        "",
dfa38cf4cd4batagen2026-10-10 09:17 UTC349
    )
dfa38cf4cd4batagen2026-10-10 09:17 UTC350
    .await;
dfa38cf4cd4batagen2026-10-10 09:17 UTC351
    assert_eq!(basic.status(), StatusCode::UNAUTHORIZED);
dfa38cf4cd4batagen2026-10-10 09:17 UTC352
    assert!(
dfa38cf4cd4batagen2026-10-10 09:17 UTC353
        basic.headers()[header::WWW_AUTHENTICATE]
dfa38cf4cd4batagen2026-10-10 09:17 UTC354
            .to_str()
dfa38cf4cd4batagen2026-10-10 09:17 UTC355
            .unwrap()
dfa38cf4cd4batagen2026-10-10 09:17 UTC356
            .starts_with("Digest")
dfa38cf4cd4batagen2026-10-10 09:17 UTC357
    );
dfa38cf4cd4batagen2026-10-10 09:17 UTC358
    let replay = send(
dfa38cf4cd4batagen2026-10-10 09:17 UTC359
        &app,
dfa38cf4cd4batagen2026-10-10 09:17 UTC360
        "GET",
dfa38cf4cd4batagen2026-10-10 09:17 UTC361
        "/runs/cafebabe",
dfa38cf4cd4batagen2026-10-10 09:17 UTC362
        &[(
dfa38cf4cd4batagen2026-10-10 09:17 UTC363
            "authorization",
dfa38cf4cd4batagen2026-10-10 09:17 UTC364
            "Digest username=\"homework\", nonce=\"abc\"",
dfa38cf4cd4batagen2026-10-10 09:17 UTC365
        )],
dfa38cf4cd4batagen2026-10-10 09:17 UTC366
        "",
dfa38cf4cd4batagen2026-10-10 09:17 UTC367
    )
dfa38cf4cd4batagen2026-10-10 09:17 UTC368
    .await;
dfa38cf4cd4batagen2026-10-10 09:17 UTC369
    assert_eq!(
dfa38cf4cd4batagen2026-10-10 09:17 UTC370
        json(replay).await["type"],
dfa38cf4cd4batagen2026-10-10 09:17 UTC371
        "https://jelq.dev/problems/excuse-replayed"
dfa38cf4cd4batagen2026-10-10 09:17 UTC372
    );
dfa38cf4cd4batagen2026-10-10 09:17 UTC373
    let fresh = send(
dfa38cf4cd4batagen2026-10-10 09:17 UTC374
        &app,
dfa38cf4cd4batagen2026-10-10 09:17 UTC375
        "GET",
dfa38cf4cd4batagen2026-10-10 09:17 UTC376
        "/runs/cafebabe",
dfa38cf4cd4batagen2026-10-10 09:17 UTC377
        &[(
dfa38cf4cd4batagen2026-10-10 09:17 UTC378
            "authorization",
dfa38cf4cd4batagen2026-10-10 09:17 UTC379
            "Digest username=\"praying\", nonce=\"abc\"",
dfa38cf4cd4batagen2026-10-10 09:17 UTC380
        )],
dfa38cf4cd4batagen2026-10-10 09:17 UTC381
        "",
dfa38cf4cd4batagen2026-10-10 09:17 UTC382
    )
dfa38cf4cd4batagen2026-10-10 09:17 UTC383
    .await;
dfa38cf4cd4batagen2026-10-10 09:17 UTC384
    assert_eq!(fresh.status(), StatusCode::OK);
dfa38cf4cd4batagen2026-10-10 09:17 UTC385
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC386
dfa38cf4cd4batagen2026-10-10 09:17 UTC387
#[tokio::test]
dfa38cf4cd4batagen2026-10-10 09:17 UTC388
async fn length_supports_inch_ranges() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC389
    let app = seeded_with(Floor::Bedroom, |_| {});
dfa38cf4cd4batagen2026-10-10 09:17 UTC390
    let response = send(
dfa38cf4cd4batagen2026-10-10 09:17 UTC391
        &app,
dfa38cf4cd4batagen2026-10-10 09:17 UTC392
        "GET",
dfa38cf4cd4batagen2026-10-10 09:17 UTC393
        "/runs/cafebabe/length",
dfa38cf4cd4batagen2026-10-10 09:17 UTC394
        &[("range", "inches=0-1")],
dfa38cf4cd4batagen2026-10-10 09:17 UTC395
        "",
dfa38cf4cd4batagen2026-10-10 09:17 UTC396
    )
dfa38cf4cd4batagen2026-10-10 09:17 UTC397
    .await;
dfa38cf4cd4batagen2026-10-10 09:17 UTC398
    assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT);
dfa38cf4cd4batagen2026-10-10 09:17 UTC399
    assert!(
dfa38cf4cd4batagen2026-10-10 09:17 UTC400
        response.headers()[header::CONTENT_RANGE]
dfa38cf4cd4batagen2026-10-10 09:17 UTC401
            .to_str()
dfa38cf4cd4batagen2026-10-10 09:17 UTC402
            .unwrap()
dfa38cf4cd4batagen2026-10-10 09:17 UTC403
            .starts_with("inches 0.00-1.00/")
dfa38cf4cd4batagen2026-10-10 09:17 UTC404
    );
dfa38cf4cd4batagen2026-10-10 09:17 UTC405
    let body = to_bytes(response.into_body(), usize::MAX).await.unwrap();
dfa38cf4cd4batagen2026-10-10 09:17 UTC406
    assert_eq!(body.len(), 25);
dfa38cf4cd4batagen2026-10-10 09:17 UTC407
    let absurd = send(
dfa38cf4cd4batagen2026-10-10 09:17 UTC408
        &app,
dfa38cf4cd4batagen2026-10-10 09:17 UTC409
        "GET",
dfa38cf4cd4batagen2026-10-10 09:17 UTC410
        "/runs/cafebabe/length",
dfa38cf4cd4batagen2026-10-10 09:17 UTC411
        &[("range", "inches=40-50")],
dfa38cf4cd4batagen2026-10-10 09:17 UTC412
        "",
dfa38cf4cd4batagen2026-10-10 09:17 UTC413
    )
dfa38cf4cd4batagen2026-10-10 09:17 UTC414
    .await;
dfa38cf4cd4batagen2026-10-10 09:17 UTC415
    assert_eq!(absurd.status(), StatusCode::RANGE_NOT_SATISFIABLE);
dfa38cf4cd4batagen2026-10-10 09:17 UTC416
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC417
dfa38cf4cd4batagen2026-10-10 09:17 UTC418
#[tokio::test]
dfa38cf4cd4batagen2026-10-10 09:17 UTC419
async fn http_1_0_gets_the_pdf_treatment() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC420
    let request = Request::builder()
dfa38cf4cd4batagen2026-10-10 09:17 UTC421
        .uri("/graveyard")
dfa38cf4cd4batagen2026-10-10 09:17 UTC422
        .version(axum::http::Version::HTTP_10)
dfa38cf4cd4batagen2026-10-10 09:17 UTC423
        .body(Body::empty())
dfa38cf4cd4batagen2026-10-10 09:17 UTC424
        .unwrap();
dfa38cf4cd4batagen2026-10-10 09:17 UTC425
    let response = jelqd::app().oneshot(request).await.unwrap();
dfa38cf4cd4batagen2026-10-10 09:17 UTC426
    assert_eq!(response.status(), StatusCode::HTTP_VERSION_NOT_SUPPORTED);
dfa38cf4cd4batagen2026-10-10 09:17 UTC427
}
177535ac8abfatagen2026-10-10 10:39 UTC428
177535ac8abfatagen2026-10-10 10:39 UTC429
#[tokio::test]
177535ac8abfatagen2026-10-10 10:39 UTC430
async fn the_displayed_length_is_accepted_as_honest() {
177535ac8abfatagen2026-10-10 10:39 UTC431
    let app = seeded_with(Floor::Forum, |run| run.length = 5.2496);
177535ac8abfatagen2026-10-10 10:39 UTC432
    let response = send(
177535ac8abfatagen2026-10-10 10:39 UTC433
        &app,
177535ac8abfatagen2026-10-10 10:39 UTC434
        "GET",
177535ac8abfatagen2026-10-10 10:39 UTC435
        "/runs/cafebabe/elder",
177535ac8abfatagen2026-10-10 10:39 UTC436
        &[("proxy-authorization", "Ruler 5.25")],
177535ac8abfatagen2026-10-10 10:39 UTC437
        "",
177535ac8abfatagen2026-10-10 10:39 UTC438
    )
177535ac8abfatagen2026-10-10 10:39 UTC439
    .await;
177535ac8abfatagen2026-10-10 10:39 UTC440
    assert_eq!(response.status(), StatusCode::OK);
177535ac8abfatagen2026-10-10 10:39 UTC441
}
99524cf9f17catagen2026-10-10 10:53 UTC442
99524cf9f17catagen2026-10-10 10:53 UTC443
#[tokio::test]
99524cf9f17catagen2026-10-10 10:53 UTC444
async fn mom_runs_out_of_patience() {
99524cf9f17catagen2026-10-10 10:53 UTC445
    let app = seeded_with(Floor::Bedroom, |run| run.mom_present = true);
99524cf9f17catagen2026-10-10 10:53 UTC446
    let lie = [("authorization", "Excuse \"it is for science\"")];
99524cf9f17catagen2026-10-10 10:53 UTC447
    for _ in 0..2 {
99524cf9f17catagen2026-10-10 10:53 UTC448
        let response = send(&app, "GET", "/runs/cafebabe", &lie, "").await;
99524cf9f17catagen2026-10-10 10:53 UTC449
        assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
99524cf9f17catagen2026-10-10 10:53 UTC450
    }
99524cf9f17catagen2026-10-10 10:53 UTC451
    let caught = send(&app, "GET", "/runs/cafebabe", &lie, "").await;
99524cf9f17catagen2026-10-10 10:53 UTC452
    assert_eq!(caught.status(), StatusCode::GONE);
99524cf9f17catagen2026-10-10 10:53 UTC453
    assert_eq!(
99524cf9f17catagen2026-10-10 10:53 UTC454
        json(caught).await["type"],
99524cf9f17catagen2026-10-10 10:53 UTC455
        "https://jelq.dev/problems/caught"
99524cf9f17catagen2026-10-10 10:53 UTC456
    );
99524cf9f17catagen2026-10-10 10:53 UTC457
}
99524cf9f17catagen2026-10-10 10:53 UTC458
99524cf9f17catagen2026-10-10 10:53 UTC459
#[tokio::test]
99524cf9f17catagen2026-10-10 10:53 UTC460
async fn forged_ruler_cookies_are_lies() {
99524cf9f17catagen2026-10-10 10:53 UTC461
    let app = jelqd::app();
99524cf9f17catagen2026-10-10 10:53 UTC462
    let forged = send(
99524cf9f17catagen2026-10-10 10:53 UTC463
        &app,
99524cf9f17catagen2026-10-10 10:53 UTC464
        "GET",
99524cf9f17catagen2026-10-10 10:53 UTC465
        "/leaderboard?sort=content-length",
99524cf9f17catagen2026-10-10 10:53 UTC466
        &[("cookie", "ruler=1")],
99524cf9f17catagen2026-10-10 10:53 UTC467
        "",
99524cf9f17catagen2026-10-10 10:53 UTC468
    )
99524cf9f17catagen2026-10-10 10:53 UTC469
    .await;
99524cf9f17catagen2026-10-10 10:53 UTC470
    assert_eq!(forged.status(), StatusCode::BAD_REQUEST);
99524cf9f17catagen2026-10-10 10:53 UTC471
}
99524cf9f17catagen2026-10-10 10:53 UTC472
99524cf9f17catagen2026-10-10 10:53 UTC473
#[tokio::test]
99524cf9f17catagen2026-10-10 10:53 UTC474
async fn seeded_runs_are_unranked() {
99524cf9f17catagen2026-10-10 10:53 UTC475
    let app = jelqd::app();
99524cf9f17catagen2026-10-10 10:53 UTC476
    send(&app, "POST", "/runs?seed=7", &[], "").await;
99524cf9f17catagen2026-10-10 10:53 UTC477
    let board = json(send(&app, "GET", "/leaderboard", &[], "").await).await;
99524cf9f17catagen2026-10-10 10:53 UTC478
    assert_eq!(board["entries"].as_array().unwrap().len(), 0);
99524cf9f17catagen2026-10-10 10:53 UTC479
    let all = json(send(&app, "GET", "/leaderboard?seeded=include", &[], "").await).await;
99524cf9f17catagen2026-10-10 10:53 UTC480
    assert_eq!(all["entries"][0]["ranked"], false);
99524cf9f17catagen2026-10-10 10:53 UTC481
}
99524cf9f17catagen2026-10-10 10:53 UTC482
99524cf9f17catagen2026-10-10 10:53 UTC483
#[tokio::test]
99524cf9f17catagen2026-10-10 10:53 UTC484
async fn the_root_negotiates() {
99524cf9f17catagen2026-10-10 10:53 UTC485
    let app = jelqd::app();
99524cf9f17catagen2026-10-10 10:53 UTC486
    let browser = send(&app, "GET", "/", &[("accept", "text/html,*/*")], "").await;
99524cf9f17catagen2026-10-10 10:53 UTC487
    assert_eq!(browser.status(), StatusCode::SEE_OTHER);
99524cf9f17catagen2026-10-10 10:53 UTC488
    assert_eq!(browser.headers()[header::LOCATION], "/ui/");
99524cf9f17catagen2026-10-10 10:53 UTC489
    let curl = send(&app, "GET", "/", &[("accept", "*/*")], "").await;
99524cf9f17catagen2026-10-10 10:53 UTC490
    assert_eq!(json(curl).await["ui"], "/ui/");
99524cf9f17catagen2026-10-10 10:53 UTC491
    let page = send(&app, "GET", "/ui/", &[], "").await;
99524cf9f17catagen2026-10-10 10:53 UTC492
    assert_eq!(page.status(), StatusCode::OK);
99524cf9f17catagen2026-10-10 10:53 UTC493
}
99524cf9f17catagen2026-10-10 10:53 UTC494
99524cf9f17catagen2026-10-10 10:53 UTC495
#[tokio::test]
99524cf9f17catagen2026-10-10 10:53 UTC496
async fn the_ui_replays_what_browsers_cannot_send() {
99524cf9f17catagen2026-10-10 10:53 UTC497
    let app = jelqd::app();
99524cf9f17catagen2026-10-10 10:53 UTC498
    let envelope =
99524cf9f17catagen2026-10-10 10:53 UTC499
        r#"{"method": "TRACE", "path": "/graveyard", "headers": [["X-Confession", "I did it"]]}"#;
99524cf9f17catagen2026-10-10 10:53 UTC500
    let traced = json(send(&app, "POST", "/ui/send", &[], envelope).await).await;
99524cf9f17catagen2026-10-10 10:53 UTC501
    assert_eq!(traced["status"], 405);
99524cf9f17catagen2026-10-10 10:53 UTC502
    let envelope = r#"{"method": "GET", "path": "/", "headers": [["Accept", "text/html"]]}"#;
99524cf9f17catagen2026-10-10 10:53 UTC503
    let redirected = json(send(&app, "POST", "/ui/send", &[], envelope).await).await;
99524cf9f17catagen2026-10-10 10:53 UTC504
    assert_eq!(redirected["status"], 303);
99524cf9f17catagen2026-10-10 10:53 UTC505
    assert_eq!(redirected["reason"], "See Other");
99524cf9f17catagen2026-10-10 10:53 UTC506
}
c3e0c27cf8fdatagen2026-10-10 11:28 UTC507
c3e0c27cf8fdatagen2026-10-10 11:28 UTC508
#[tokio::test]
c3e0c27cf8fdatagen2026-10-10 11:28 UTC509
async fn the_internet_never_forgets() {
c3e0c27cf8fdatagen2026-10-10 11:28 UTC510
    let app = seeded_with(Floor::Bedroom, |run| {
c3e0c27cf8fdatagen2026-10-10 11:28 UTC511
        run.filmed_at = Some(std::time::SystemTime::now());
c3e0c27cf8fdatagen2026-10-10 11:28 UTC512
    });
c3e0c27cf8fdatagen2026-10-10 11:28 UTC513
    let footage = send(&app, "GET", "/runs/cafebabe/footage", &[], "").await;
c3e0c27cf8fdatagen2026-10-10 11:28 UTC514
    assert_eq!(footage.status(), StatusCode::OK);
c3e0c27cf8fdatagen2026-10-10 11:28 UTC515
    assert_eq!(
c3e0c27cf8fdatagen2026-10-10 11:28 UTC516
        footage.headers()[header::CACHE_CONTROL],
c3e0c27cf8fdatagen2026-10-10 11:28 UTC517
        "public, max-age=31536000, immutable"
c3e0c27cf8fdatagen2026-10-10 11:28 UTC518
    );
c3e0c27cf8fdatagen2026-10-10 11:28 UTC519
    assert!(footage.headers().contains_key(header::AGE));
c3e0c27cf8fdatagen2026-10-10 11:28 UTC520
    let delete = send(&app, "DELETE", "/runs/cafebabe/footage", &[], "").await;
c3e0c27cf8fdatagen2026-10-10 11:28 UTC521
    assert_eq!(delete.status(), StatusCode::METHOD_NOT_ALLOWED);
c3e0c27cf8fdatagen2026-10-10 11:28 UTC522
}
c3e0c27cf8fdatagen2026-10-10 11:28 UTC523
c3e0c27cf8fdatagen2026-10-10 11:28 UTC524
#[tokio::test]
c3e0c27cf8fdatagen2026-10-10 11:28 UTC525
async fn a_pulled_muscle_is_a_true_excuse() {
c3e0c27cf8fdatagen2026-10-10 11:28 UTC526
    let app = seeded_with(Floor::Grandmas, |run| {
c3e0c27cf8fdatagen2026-10-10 11:28 UTC527
        run.mom_present = true;
c3e0c27cf8fdatagen2026-10-10 11:28 UTC528
        run.nonce = "abc".into();
c3e0c27cf8fdatagen2026-10-10 11:28 UTC529
        run.pulled_muscle = true;
c3e0c27cf8fdatagen2026-10-10 11:28 UTC530
        run.heard.push("pulled a muscle".into());
c3e0c27cf8fdatagen2026-10-10 11:28 UTC531
    });
c3e0c27cf8fdatagen2026-10-10 11:28 UTC532
    let response = send(
c3e0c27cf8fdatagen2026-10-10 11:28 UTC533
        &app,
c3e0c27cf8fdatagen2026-10-10 11:28 UTC534
        "GET",
c3e0c27cf8fdatagen2026-10-10 11:28 UTC535
        "/runs/cafebabe",
c3e0c27cf8fdatagen2026-10-10 11:28 UTC536
        &[(
c3e0c27cf8fdatagen2026-10-10 11:28 UTC537
            "authorization",
c3e0c27cf8fdatagen2026-10-10 11:28 UTC538
            "Digest username=\"pulled a muscle\", nonce=\"abc\"",
c3e0c27cf8fdatagen2026-10-10 11:28 UTC539
        )],
c3e0c27cf8fdatagen2026-10-10 11:28 UTC540
        "",
c3e0c27cf8fdatagen2026-10-10 11:28 UTC541
    )
c3e0c27cf8fdatagen2026-10-10 11:28 UTC542
    .await;
c3e0c27cf8fdatagen2026-10-10 11:28 UTC543
    assert_eq!(response.status(), StatusCode::OK);
c3e0c27cf8fdatagen2026-10-10 11:28 UTC544
    assert!(
c3e0c27cf8fdatagen2026-10-10 11:28 UTC545
        response.headers()["x-stamina"]
c3e0c27cf8fdatagen2026-10-10 11:28 UTC546
            .to_str()
c3e0c27cf8fdatagen2026-10-10 11:28 UTC547
            .unwrap()
c3e0c27cf8fdatagen2026-10-10 11:28 UTC548
            .ends_with("/7")
c3e0c27cf8fdatagen2026-10-10 11:28 UTC549
    );
c3e0c27cf8fdatagen2026-10-10 11:28 UTC550
}
ba879f96510datagen2026-10-10 12:03 UTC551
ba879f96510datagen2026-10-10 12:03 UTC552
#[tokio::test]
ba879f96510datagen2026-10-10 12:03 UTC553
async fn the_ui_decodes_gzip_for_display() {
ba879f96510datagen2026-10-10 12:03 UTC554
    let app = seeded_with(Floor::Bedroom, |run| run.items.ring = true);
ba879f96510datagen2026-10-10 12:03 UTC555
    let envelope =
ba879f96510datagen2026-10-10 12:03 UTC556
        r#"{"method": "GET", "path": "/runs/cafebabe", "headers": [["Accept-Encoding", "gzip"]]}"#;
ba879f96510datagen2026-10-10 12:03 UTC557
    let shown = json(send(&app, "POST", "/ui/send", &[], envelope).await).await;
ba879f96510datagen2026-10-10 12:03 UTC558
    assert!(
ba879f96510datagen2026-10-10 12:03 UTC559
        shown["body"]
ba879f96510datagen2026-10-10 12:03 UTC560
            .as_str()
ba879f96510datagen2026-10-10 12:03 UTC561
            .unwrap()
ba879f96510datagen2026-10-10 12:03 UTC562
            .contains("\"id\":\"cafebabe\"")
ba879f96510datagen2026-10-10 12:03 UTC563
    );
ba879f96510datagen2026-10-10 12:03 UTC564
    let encoding = shown["headers"]
ba879f96510datagen2026-10-10 12:03 UTC565
        .as_array()
ba879f96510datagen2026-10-10 12:03 UTC566
        .unwrap()
ba879f96510datagen2026-10-10 12:03 UTC567
        .iter()
ba879f96510datagen2026-10-10 12:03 UTC568
        .any(|h| h[0] == "content-encoding" && h[1] == "gzip");
ba879f96510datagen2026-10-10 12:03 UTC569
    assert!(encoding);
ba879f96510datagen2026-10-10 12:03 UTC570
}