atagen

jelq

public

an http based roguelike rpg about jelqing

Star0
Watch0
e807266c323bcc1fd00f59aa160b3b94d8168beb
1
ssh://brewery@pls.lobotomise.me:33/atagen/jelq.git
src/tests.rsRust1895 lines62.3 KB
1
use std::{
2
    sync::Arc,
3
    time::{Duration, Instant},
4
};
5
6
use axum::{
7
    Router,
8
    body::{Body, to_bytes},
9
    http::{Method, Request, StatusCode, Version, header},
10
    response::Response,
11
};
12
use serde_json::Value;
13
use tower::ServiceExt;
14
15
use crate::{
16
    AppState, Config,
17
    game::{
18
        Floor, MumVisit, Run, Story,
19
        balance::{BRANCH_TURN, STORAGE_GIRTH, SUSPICION_LIMIT},
20
    },
21
    router,
22
};
23
24
async fn send(
25
    app: &Router,
26
    method: &str,
27
    uri: &str,
28
    headers: &[(&str, &str)],
29
    body: &str,
30
) -> Response {
31
    let mut request = Request::builder()
32
        .method(Method::from_bytes(method.as_bytes()).unwrap())
33
        .uri(uri);
34
    for (name, value) in headers {
35
        request = request.header(*name, *value);
36
    }
37
    app.clone()
38
        .oneshot(request.body(Body::from(body.to_owned())).unwrap())
39
        .await
40
        .unwrap()
41
}
42
43
async fn json(response: Response) -> Value {
44
    serde_json::from_slice(&to_bytes(response.into_body(), usize::MAX).await.unwrap()).unwrap()
45
}
46
47
async fn text(response: Response) -> String {
48
    String::from_utf8(
49
        to_bytes(response.into_body(), usize::MAX)
50
            .await
51
            .unwrap()
52
            .to_vec(),
53
    )
54
    .unwrap()
55
}
56
57
fn header<'a>(response: &'a Response, name: &str) -> &'a str {
58
    response.headers()[name].to_str().unwrap()
59
}
60
61
fn fixture() -> Run {
62
    Run::new("cafebabe".into(), "cafe".into(), 69)
63
}
64
65
fn seeded_with(floor: Floor, setup: impl FnOnce(&mut Run)) -> Router {
66
    let state = Arc::new(AppState::default());
67
    let mut run = fixture();
68
    run.floor = floor;
69
    setup(&mut run);
70
    state.insert(run);
71
    router(state)
72
}
73
74
fn seeded(floor: Floor) -> (Router, String) {
75
    let state = Arc::new(AppState::default());
76
    let mut run = fixture();
77
    run.floor = floor;
78
    let etag = run.origin_etag();
79
    state.insert(run);
80
    (router(state), etag)
81
}
82
83
fn app() -> Router {
84
    crate::app(Config::default())
85
}
86
87
fn mum_at_the_door(run: &mut Run) {
88
    run.mum = Some(MumVisit {
89
        errand: "She heard a noise.",
90
        nonce: "abc".into(),
91
        strikes: 0,
92
        knocked: false,
93
    });
94
}
95
96
#[tokio::test]
97
async fn creating_a_run_returns_location_and_etag() {
98
    let response = send(&app(), "POST", "/runs?seed=1", &[], "").await;
99
    assert_eq!(response.status(), StatusCode::CREATED);
100
    assert!(header(&response, "location").starts_with("/runs/"));
101
    assert!(response.headers().contains_key(header::ETAG));
102
    assert_eq!(header(&response, "server"), "jelqd/0.6.9");
103
}
104
105
#[tokio::test]
106
async fn bodies_do_not_repeat_the_headers() {
107
    let (app, _) = seeded(Floor::Bedroom);
108
    let response = send(&app, "GET", "/runs/cafebabe", &[], "").await;
109
    assert!(response.headers().contains_key("x-eq"));
110
    let body = json(response).await;
111
    for stat in [
112
        "eq",
113
        "girth",
114
        "foreskin",
115
        "stamina",
116
        "cash",
117
        "self_reported_length",
118
    ] {
119
        assert!(body.get(stat).is_none(), "{stat} is already a header");
120
    }
121
}
122
123
#[tokio::test]
124
async fn jelqing_cold_fails_the_precondition() {
125
    let (app, _) = seeded(Floor::Bedroom);
126
    let response = send(&app, "JELQ", "/runs/cafebabe?technique=stretch", &[], "").await;
127
    assert_eq!(response.status(), StatusCode::PRECONDITION_FAILED);
128
    assert_eq!(
129
        header(&response, "content-type"),
130
        "application/problem+json"
131
    );
132
}
133
134
#[tokio::test]
135
async fn wet_without_lube_is_unsupported_media() {
136
    let (app, _) = seeded(Floor::Bedroom);
137
    send(&app, "JELQ", "/runs/cafebabe?technique=warmup", &[], "").await;
138
    let response = send(&app, "JELQ", "/runs/cafebabe?technique=wet", &[], "").await;
139
    assert_eq!(response.status(), StatusCode::UNSUPPORTED_MEDIA_TYPE);
140
    let images = send(
141
        &app,
142
        "JELQ",
143
        "/runs/cafebabe?technique=wet",
144
        &[("content-type", "image/jpeg")],
145
        "",
146
    )
147
    .await;
148
    assert_eq!(json(images).await["detail"], "No.");
149
}
150
151
#[tokio::test]
152
async fn unknown_methods_are_not_implemented() {
153
    let (app, _) = seeded(Floor::Bedroom);
154
    let response = send(&app, "MILK", "/runs/cafebabe", &[], "").await;
155
    assert_eq!(response.status(), StatusCode::NOT_IMPLEMENTED);
156
    let teapot = send(&app, "BREW", "/runs/cafebabe", &[], "").await;
157
    assert_eq!(teapot.status(), StatusCode::IM_A_TEAPOT);
158
}
159
160
#[tokio::test]
161
async fn the_clinic_does_not_advertise_jelq() {
162
    let (app, _) = seeded(Floor::Clinic);
163
    let options = send(&app, "OPTIONS", "/runs/cafebabe", &[], "").await;
164
    assert!(!header(&options, "allow").contains("JELQ"));
165
    let jelq = send(&app, "JELQ", "/runs/cafebabe?technique=wet", &[], "").await;
166
    assert_eq!(jelq.status(), StatusCode::METHOD_NOT_ALLOWED);
167
    assert!(!header(&jelq, "allow").contains("JELQ"));
168
    let (bedroom, _) = seeded(Floor::Bedroom);
169
    let options = send(&bedroom, "OPTIONS", "/runs/cafebabe", &[], "").await;
170
    assert!(header(&options, "allow").contains("JELQ"));
171
}
172
173
#[tokio::test]
174
async fn patch_only_accepts_the_truth() {
175
    let (app, _) = seeded(Floor::Bedroom);
176
    let patch = [("content-type", "application/json-patch+json")];
177
    let replace = send(
178
        &app,
179
        "PATCH",
180
        "/runs/cafebabe",
181
        &patch,
182
        r#"[{"op":"replace","path":"/length","value":12}]"#,
183
    )
184
    .await;
185
    assert_eq!(replace.status(), StatusCode::UNPROCESSABLE_ENTITY);
186
    let lie = send(
187
        &app,
188
        "PATCH",
189
        "/runs/cafebabe",
190
        &patch,
191
        r#"[{"op":"test","path":"/length","value":40}]"#,
192
    )
193
    .await;
194
    assert_eq!(
195
        json(lie).await["type"],
196
        "https://jelq.dev/problems/exaggeration"
197
    );
198
}
199
200
#[tokio::test]
201
async fn deleting_the_foreskin_requires_certainty() {
202
    let (app, _) = seeded(Floor::Bedroom);
203
    let response = send(&app, "DELETE", "/runs/cafebabe/foreskin", &[], "").await;
204
    assert_eq!(response.status(), StatusCode::PRECONDITION_REQUIRED);
205
    let look = send(&app, "GET", "/runs/cafebabe/foreskin", &[], "").await;
206
    assert_eq!(look.status(), StatusCode::OK);
207
    let etag = header(&look, "etag").to_owned();
208
    assert!(text(look).await.contains('~'));
209
    let response = send(
210
        &app,
211
        "DELETE",
212
        "/runs/cafebabe/foreskin",
213
        &[("if-match", &etag)],
214
        "",
215
    )
216
    .await;
217
    assert_eq!(response.status(), StatusCode::NO_CONTENT);
218
    let response = send(&app, "GET", "/runs/cafebabe/foreskin", &[], "").await;
219
    assert_eq!(response.status(), StatusCode::GONE);
220
}
221
222
#[tokio::test]
223
async fn length_is_chunked_until_the_ruler() {
224
    let (app, _) = seeded(Floor::Bedroom);
225
    let lie = send(&app, "GET", "/runs/cafebabe/length", &[], "").await;
226
    assert!(!lie.headers().contains_key(header::CONTENT_LENGTH));
227
    let app = seeded_with(Floor::Bedroom, |run| run.items.ruler = true);
228
    let truth = send(&app, "GET", "/runs/cafebabe/length", &[], "").await;
229
    let declared: usize = header(&truth, "content-length").parse().unwrap();
230
    let body = text(truth).await;
231
    assert_eq!(body.len(), declared);
232
    assert!(body.starts_with("8=") && body.ends_with('D'));
233
}
234
235
#[tokio::test]
236
async fn length_supports_inch_ranges() {
237
    let (app, _) = seeded(Floor::Bedroom);
238
    let response = send(
239
        &app,
240
        "GET",
241
        "/runs/cafebabe/length",
242
        &[("range", "inches=0-1")],
243
        "",
244
    )
245
    .await;
246
    assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT);
247
    assert!(header(&response, "content-range").starts_with("inches 0.00-1.00/"));
248
    assert_eq!(text(response).await.len(), 25);
249
    let absurd = send(
250
        &app,
251
        "GET",
252
        "/runs/cafebabe/length",
253
        &[("range", "inches=40-50")],
254
        "",
255
    )
256
    .await;
257
    assert_eq!(absurd.status(), StatusCode::RANGE_NOT_SATISFIABLE);
258
    let malformed = send(
259
        &app,
260
        "GET",
261
        "/runs/cafebabe/length",
262
        &[("range", "inches=6.17")],
263
        "",
264
    )
265
    .await;
266
    assert_eq!(malformed.status(), StatusCode::OK);
267
    let suffix = send(
268
        &app,
269
        "GET",
270
        "/runs/cafebabe/length",
271
        &[("range", "inches=-1")],
272
        "",
273
    )
274
    .await;
275
    assert_eq!(suffix.status(), StatusCode::PARTIAL_CONTENT);
276
    assert!(text(suffix).await.ends_with('D'));
277
    for (range, status) in [
278
        ("inches=1-", StatusCode::PARTIAL_CONTENT),
279
        ("inches=40-", StatusCode::RANGE_NOT_SATISFIABLE),
280
        ("inches=-0", StatusCode::RANGE_NOT_SATISFIABLE),
281
        ("inches=2-1", StatusCode::OK),
282
        ("inches=NaN-2", StatusCode::OK),
283
        ("inches=0-inf", StatusCode::OK),
284
    ] {
285
        let response = send(
286
            &app,
287
            "GET",
288
            "/runs/cafebabe/length",
289
            &[("range", range)],
290
            "",
291
        )
292
        .await;
293
        assert_eq!(response.status(), status, "{range}");
294
    }
295
}
296
297
#[tokio::test]
298
async fn the_clinic_escalates_toward_honesty() {
299
    let (app, _) = seeded(Floor::Clinic);
300
    let mut hints = Vec::new();
301
    for _ in 0..3 {
302
        let response = send(
303
            &app,
304
            "GET",
305
            "/runs/cafebabe/diagnosis",
306
            &[("authorization", "Excuse \"changing\"")],
307
            "",
308
        )
309
        .await;
310
        assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
311
        hints.push(header(&response, "www-authenticate").to_owned());
312
    }
313
    assert!(hints[2].contains("Be honest."));
314
}
315
316
#[tokio::test]
317
async fn the_true_ending_is_not_modified() {
318
    let (early, _) = seeded(Floor::Bedroom);
319
    let response = send(&early, "GET", "/runs/cafebabe/diagnosis", &[], "").await;
320
    assert_eq!(response.status(), StatusCode::TOO_EARLY);
321
322
    let (app, etag) = seeded(Floor::Clinic);
323
    assert!(
324
        !etag.contains("cafebabe"),
325
        "the origin ETag must not be derivable from the id"
326
    );
327
    let response = send(
328
        &app,
329
        "GET",
330
        "/runs/cafebabe/diagnosis",
331
        &[
332
            ("authorization", "Bearer honesty"),
333
            ("if-none-match", &etag),
334
        ],
335
        "",
336
    )
337
    .await;
338
    assert_eq!(response.status(), StatusCode::NOT_MODIFIED);
339
    assert_eq!(header(&response, "x-diagnosis"), "normal");
340
}
341
342
#[tokio::test]
343
async fn mum_accepts_a_known_excuse() {
344
    let app = seeded_with(Floor::Bedroom, mum_at_the_door);
345
    let response = send(&app, "GET", "/runs/cafebabe", &[], "").await;
346
    assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
347
    assert_eq!(header(&response, "via"), "1.1 mum");
348
    let response = send(
349
        &app,
350
        "GET",
351
        "/runs/cafebabe",
352
        &[("authorization", "Excuse \"homework\"")],
353
        "",
354
    )
355
    .await;
356
    assert_eq!(response.status(), StatusCode::OK);
357
}
358
359
#[tokio::test]
360
async fn mum_runs_out_of_patience() {
361
    let app = seeded_with(Floor::Bedroom, mum_at_the_door);
362
    let lie = [("authorization", "Excuse \"it is for science\"")];
363
    for _ in 0..2 {
364
        let response = send(&app, "GET", "/runs/cafebabe", &lie, "").await;
365
        assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
366
    }
367
    let caught = send(&app, "GET", "/runs/cafebabe", &lie, "").await;
368
    assert_eq!(caught.status(), StatusCode::GONE);
369
    assert_eq!(
370
        json(caught).await["type"],
371
        "https://jelq.dev/problems/caught"
372
    );
373
}
374
375
#[tokio::test]
376
async fn mum_remembers_excuses_by_christmas() {
377
    let app = seeded_with(Floor::Nans, |run| {
378
        mum_at_the_door(run);
379
        run.heard.push("homework".into());
380
    });
381
    let basic = send(
382
        &app,
383
        "GET",
384
        "/runs/cafebabe",
385
        &[("authorization", "Excuse \"changing\"")],
386
        "",
387
    )
388
    .await;
389
    assert_eq!(basic.status(), StatusCode::UNAUTHORIZED);
390
    assert!(header(&basic, "www-authenticate").starts_with("Digest"));
391
    let replay = send(
392
        &app,
393
        "GET",
394
        "/runs/cafebabe",
395
        &[(
396
            "authorization",
397
            "Digest username=\"homework\", nonce=\"abc\"",
398
        )],
399
        "",
400
    )
401
    .await;
402
    assert_eq!(
403
        json(replay).await["type"],
404
        "https://jelq.dev/problems/excuse-replayed"
405
    );
406
    let fresh = send(
407
        &app,
408
        "GET",
409
        "/runs/cafebabe",
410
        &[(
411
            "authorization",
412
            "Digest username=\"wrapping presents\", nonce=\"abc\"",
413
        )],
414
        "",
415
    )
416
    .await;
417
    assert_eq!(fresh.status(), StatusCode::OK);
418
}
419
420
#[tokio::test]
421
async fn a_pulled_muscle_is_a_true_excuse() {
422
    let app = seeded_with(Floor::Nans, |run| {
423
        mum_at_the_door(run);
424
        run.conditions.pulled_muscle = true;
425
        run.heard.push("pulled a muscle".into());
426
    });
427
    let response = send(
428
        &app,
429
        "GET",
430
        "/runs/cafebabe",
431
        &[(
432
            "authorization",
433
            "Digest username=\"pulled a muscle\", nonce=\"abc\"",
434
        )],
435
        "",
436
    )
437
    .await;
438
    assert_eq!(response.status(), StatusCode::OK);
439
    assert!(header(&response, "x-stamina").ends_with("/7"));
440
}
441
442
#[tokio::test]
443
async fn floors_end_when_they_end() {
444
    let app = seeded_with(Floor::Bedroom, |_| {});
445
    let early = send(&app, "POST", "/runs/cafebabe/floor", &[], "").await;
446
    assert_eq!(early.status(), StatusCode::TOO_EARLY);
447
    let app = seeded_with(Floor::Bedroom, |run| run.here.turns = 6);
448
    let next = send(&app, "POST", "/runs/cafebabe/floor", &[], "").await;
449
    assert!(
450
        next.status() == StatusCode::SEE_OTHER || next.status() == StatusCode::CONFLICT,
451
        "either the floor changed or the zip did"
452
    );
453
}
454
455
#[tokio::test]
456
async fn pump_bro_upsells() {
457
    let closed = seeded_with(Floor::Bedroom, |_| {});
458
    let response = send(&closed, "GET", "/runs/cafebabe/shop", &[], "").await;
459
    assert_eq!(response.status(), StatusCode::NOT_FOUND);
460
461
    let app = seeded_with(Floor::ChangingRoom, |_| {});
462
    let pump = send(&app, "POST", "/runs/cafebabe/shop/pump", &[], "").await;
463
    assert_eq!(pump.status(), StatusCode::UPGRADE_REQUIRED);
464
    assert_eq!(header(&pump, "upgrade"), "pump-pro/2.0");
465
    let pro = send(&app, "POST", "/runs/cafebabe/shop/pump-pro", &[], "").await;
466
    assert_eq!(pro.status(), StatusCode::CREATED);
467
    let broke = send(&app, "POST", "/runs/cafebabe/shop/extender", &[], "").await;
468
    assert_eq!(broke.status(), StatusCode::PAYMENT_REQUIRED);
469
}
470
471
#[tokio::test]
472
async fn only_the_key_opens_the_cage() {
473
    let app = seeded_with(Floor::ChangingRoom, |run| run.items.cage = true);
474
    send(&app, "POST", "/runs/cafebabe/shop/ice-pack", &[], "").await;
475
    let still = send(
476
        &app,
477
        "GET",
478
        "/runs/cafebabe/inventory/chastity-cage",
479
        &[],
480
        "",
481
    )
482
    .await;
483
    assert_eq!(still.status(), StatusCode::OK);
484
    send(&app, "POST", "/runs/cafebabe/shop/cage-key", &[], "").await;
485
    let gone = send(
486
        &app,
487
        "GET",
488
        "/runs/cafebabe/inventory/chastity-cage",
489
        &[],
490
        "",
491
    )
492
    .await;
493
    assert_eq!(gone.status(), StatusCode::NOT_FOUND);
494
}
495
496
#[tokio::test]
497
async fn combos_are_multi_status_xml() {
498
    let app = seeded_with(Floor::Bathroom, |run| {
499
        run.foreskin = Some(1.0);
500
        run.items.blinds = true;
501
    });
502
    send(&app, "JELQ", "/runs/cafebabe?technique=warmup", &[], "").await;
503
    let response = send(
504
        &app,
505
        "JELQ",
506
        "/runs/cafebabe?technique=wet,stretch",
507
        &[("content-type", "application/baby-oil")],
508
        "",
509
    )
510
    .await;
511
    assert_eq!(response.status(), StatusCode::MULTI_STATUS);
512
    assert!(text(response).await.contains("<D:multistatus"));
513
}
514
515
#[tokio::test]
516
async fn the_elder_is_stale() {
517
    let app = seeded_with(Floor::Forum, |run| run.length = 5.0);
518
    let unverified = send(&app, "GET", "/runs/cafebabe/elder", &[], "").await;
519
    assert_eq!(
520
        unverified.status(),
521
        StatusCode::PROXY_AUTHENTICATION_REQUIRED
522
    );
523
    let lie = send(
524
        &app,
525
        "GET",
526
        "/runs/cafebabe/elder",
527
        &[("proxy-authorization", "Ruler 9")],
528
        "",
529
    )
530
    .await;
531
    assert_eq!(lie.status(), StatusCode::BAD_REQUEST);
532
    let csv = send(
533
        &app,
534
        "GET",
535
        "/runs/cafebabe/elder",
536
        &[("proxy-authorization", "Ruler 5.0")],
537
        "",
538
    )
539
    .await;
540
    assert_eq!(csv.status(), StatusCode::OK);
541
    let stale = send(
542
        &app,
543
        "GET",
544
        "/runs/cafebabe/elder",
545
        &[("if-modified-since", "Mon, 15 Jun 2009 00:00:00 GMT")],
546
        "",
547
    )
548
    .await;
549
    assert_eq!(stale.status(), StatusCode::NOT_MODIFIED);
550
}
551
552
#[tokio::test]
553
async fn the_displayed_length_is_accepted_as_honest() {
554
    let app = seeded_with(Floor::Forum, |run| run.length = 5.2496);
555
    let response = send(
556
        &app,
557
        "GET",
558
        "/runs/cafebabe/elder",
559
        &[("proxy-authorization", "Ruler 5.25")],
560
        "",
561
    )
562
    .await;
563
    assert_eq!(response.status(), StatusCode::OK);
564
}
565
566
#[tokio::test]
567
async fn the_hotel_has_a_captive_portal() {
568
    let app = seeded_with(Floor::Hotel, |_| {});
569
    let blocked = send(&app, "GET", "/runs/cafebabe", &[], "").await;
570
    assert_eq!(
571
        blocked.status(),
572
        StatusCode::NETWORK_AUTHENTICATION_REQUIRED
573
    );
574
    let accepted = send(&app, "POST", "/runs/cafebabe/portal", &[], "").await;
575
    assert_eq!(accepted.status(), StatusCode::SEE_OTHER);
576
    let online = send(&app, "GET", "/runs/cafebabe", &[], "").await;
577
    assert_eq!(online.status(), StatusCode::OK);
578
}
579
580
#[tokio::test]
581
async fn the_internet_never_forgets() {
582
    let app = seeded_with(Floor::Bedroom, |run| {
583
        run.filmed_at = Some(std::time::SystemTime::now());
584
    });
585
    let footage = send(&app, "GET", "/runs/cafebabe/footage", &[], "").await;
586
    assert_eq!(footage.status(), StatusCode::OK);
587
    assert_eq!(
588
        header(&footage, "cache-control"),
589
        "public, max-age=31536000, immutable"
590
    );
591
    assert!(footage.headers().contains_key(header::AGE));
592
    let delete = send(&app, "DELETE", "/runs/cafebabe/footage", &[], "").await;
593
    assert_eq!(delete.status(), StatusCode::METHOD_NOT_ALLOWED);
594
    assert!(!header(&delete, "allow").contains("DELETE"));
595
}
596
597
#[tokio::test]
598
async fn forged_ruler_cookies_are_lies() {
599
    let forged = send(
600
        &app(),
601
        "GET",
602
        "/leaderboard?sort=content-length",
603
        &[("cookie", "ruler=1")],
604
        "",
605
    )
606
    .await;
607
    assert_eq!(forged.status(), StatusCode::BAD_REQUEST);
608
}
609
610
#[tokio::test]
611
async fn listings_never_reveal_the_private_id() {
612
    let app = app();
613
    let created = send(&app, "POST", "/runs", &[], "").await;
614
    let location = header(&created, "location").to_owned();
615
    let board = text(send(&app, "GET", "/leaderboard", &[], "").await).await;
616
    let private = location.trim_start_matches("/runs/");
617
    assert!(!board.contains(private));
618
    let entry: Value = serde_json::from_str(&board).unwrap();
619
    let public = entry["entries"][0]["public_id"].as_str().unwrap();
620
    let guess = send(&app, "DELETE", &format!("/runs/{public}"), &[], "").await;
621
    assert_eq!(guess.status(), StatusCode::NOT_FOUND);
622
}
623
624
#[tokio::test]
625
async fn seeded_runs_are_unranked() {
626
    let app = app();
627
    send(&app, "POST", "/runs?seed=7", &[], "").await;
628
    let board = json(send(&app, "GET", "/leaderboard", &[], "").await).await;
629
    assert_eq!(board["entries"].as_array().unwrap().len(), 0);
630
    let all = json(send(&app, "GET", "/leaderboard?seeded=include", &[], "").await).await;
631
    assert_eq!(all["entries"][0]["ranked"], false);
632
}
633
634
#[tokio::test]
635
async fn idle_runs_are_evicted_and_the_server_has_a_limit() {
636
    let forgetful = crate::app(Config {
637
        idle_ttl: Duration::ZERO,
638
        ..Config::default()
639
    });
640
    let first = send(&forgetful, "POST", "/runs", &[], "").await;
641
    let location = header(&first, "location").to_owned();
642
    send(&forgetful, "POST", "/runs", &[], "").await;
643
    let gone = send(&forgetful, "GET", &location, &[], "").await;
644
    assert_eq!(gone.status(), StatusCode::NOT_FOUND);
645
646
    let tiny = crate::app(Config {
647
        max_runs: 1,
648
        ..Config::default()
649
    });
650
    send(&tiny, "POST", "/runs", &[], "").await;
651
    let full = send(&tiny, "POST", "/runs", &[], "").await;
652
    assert_eq!(full.status(), StatusCode::SERVICE_UNAVAILABLE);
653
}
654
655
#[tokio::test]
656
async fn shame_from_two_runs_adds_up() {
657
    let state = Arc::new(AppState::default());
658
    state.insert(fixture());
659
    state.insert(Run::new("deadbeef".into(), "dead".into(), 7));
660
    let app = router(state);
661
    let first = send(&app, "TRACE", "/runs/cafebabe", &[], "").await;
662
    assert_eq!(header(&first, "x-shame"), "1");
663
    let cookie = header(&first, "set-cookie")
664
        .split(';')
665
        .next()
666
        .unwrap()
667
        .to_owned();
668
    let second = send(&app, "TRACE", "/runs/deadbeef", &[("cookie", &cookie)], "").await;
669
    assert_eq!(header(&second, "x-shame"), "2");
670
}
671
672
#[tokio::test]
673
async fn the_root_negotiates() {
674
    let app = app();
675
    let browser = send(&app, "GET", "/", &[("accept", "text/html,*/*")], "").await;
676
    assert_eq!(browser.status(), StatusCode::SEE_OTHER);
677
    assert_eq!(header(&browser, "location"), "/ui/");
678
    let curl = send(&app, "GET", "/", &[("accept", "*/*")], "").await;
679
    assert_eq!(json(curl).await["ui"], "/ui/");
680
    let page = send(&app, "GET", "/ui/", &[], "").await;
681
    assert_eq!(page.status(), StatusCode::OK);
682
}
683
684
#[tokio::test]
685
async fn the_ui_replays_what_browsers_cannot_send() {
686
    let app = app();
687
    let envelope =
688
        r#"{"method": "TRACE", "path": "/graveyard", "headers": [["X-Confession", "I did it"]]}"#;
689
    let traced = json(send(&app, "POST", "/ui/send", &[], envelope).await).await;
690
    assert_eq!(traced["status"], 405);
691
    let envelope = r#"{"method": "GET", "path": "/", "headers": [["Accept", "text/html"]]}"#;
692
    let redirected = json(send(&app, "POST", "/ui/send", &[], envelope).await).await;
693
    assert_eq!(redirected["status"], 303);
694
    assert_eq!(redirected["reason"], "See Other");
695
}
696
697
#[tokio::test]
698
async fn the_ui_decodes_gzip_for_display() {
699
    let app = seeded_with(Floor::Bedroom, |run| run.items.ring = true);
700
    let envelope =
701
        r#"{"method": "GET", "path": "/runs/cafebabe", "headers": [["Accept-Encoding", "gzip"]]}"#;
702
    let shown = json(send(&app, "POST", "/ui/send", &[], envelope).await).await;
703
    assert!(
704
        shown["body"]
705
            .as_str()
706
            .unwrap()
707
            .contains("\"public_id\":\"cafe\"")
708
    );
709
    let encoding = shown["headers"]
710
        .as_array()
711
        .unwrap()
712
        .iter()
713
        .any(|h| h[0] == "content-encoding" && h[1] == "gzip");
714
    assert!(encoding);
715
}
716
717
#[tokio::test]
718
async fn http_1_0_gets_the_pdf_treatment() {
719
    let request = Request::builder()
720
        .uri("/graveyard")
721
        .version(Version::HTTP_10)
722
        .body(Body::empty())
723
        .unwrap();
724
    let response = app().oneshot(request).await.unwrap();
725
    assert_eq!(response.status(), StatusCode::HTTP_VERSION_NOT_SUPPORTED);
726
}
727
728
#[tokio::test]
729
async fn mom_speaks_american() {
730
    let app = seeded_with(Floor::Nans, mum_at_the_door);
731
    let us = [("accept-language", "en-US")];
732
    let knock = send(&app, "GET", "/runs/cafebabe", &us, "").await;
733
    assert_eq!(header(&knock, "content-language"), "en-US");
734
    assert_eq!(header(&knock, "via"), "1.1 mom");
735
    assert!(
736
        json(knock).await["detail"]
737
            .as_str()
738
            .unwrap()
739
            .contains("Someone is at the door.")
740
    );
741
    let excuse = [
742
        ("accept-language", "en-US"),
743
        (
744
            "authorization",
745
            "Digest username=\"carving the turkey\", nonce=\"abc\"",
746
        ),
747
    ];
748
    let answered = send(&app, "GET", "/runs/cafebabe", &excuse, "").await;
749
    assert_eq!(answered.status(), StatusCode::OK);
750
    let floor = json(send(&app, "GET", "/runs/cafebabe/floor", &us, "").await).await;
751
    assert_eq!(floor["name"], "Grandma's house for Thanksgiving");
752
}
753
754
#[tokio::test]
755
async fn the_default_locale_is_british() {
756
    let (app, _) = seeded(Floor::Nans);
757
    let floor = send(&app, "GET", "/runs/cafebabe/floor", &[], "").await;
758
    assert_eq!(header(&floor, "content-language"), "en-GB");
759
    assert!(header(&floor, "vary").contains("Accept-Language"));
760
    assert_eq!(json(floor).await["name"], "Nan's house at Christmas");
761
}
762
763
fn advertised(response: &Response) -> Vec<String> {
764
    header(response, "link")
765
        .split(", <")
766
        .filter(|entry| entry.contains("rels/technique"))
767
        .filter_map(|entry| {
768
            entry
769
                .split("title=\"")
770
                .nth(1)?
771
                .split('"')
772
                .next()
773
                .map(str::to_owned)
774
        })
775
        .collect()
776
}
777
778
#[tokio::test]
779
async fn links_offer_only_usable_techniques() {
780
    let app = seeded_with(Floor::Bedroom, |run| run.foreskin = Some(1.0));
781
    let cold = send(&app, "GET", "/runs/cafebabe", &[], "").await;
782
    assert_eq!(advertised(&cold), ["warmup", "ice", "meditate"]);
783
    let warm = send(&app, "JELQ", "/runs/cafebabe?technique=warmup", &[], "").await;
784
    let usable = advertised(&warm);
785
    assert!(usable.contains(&"wet".to_owned()) && usable.contains(&"dry".to_owned()));
786
    assert!(!usable.contains(&"uli".to_owned()), "uli needs the forum");
787
    assert!(!usable.contains(&"pump".to_owned()), "pump needs a pump");
788
    assert!(
789
        header(&warm, "link").contains("title=\"wet\"; allow=\"JELQ\"; lubricant=\"required\"")
790
    );
791
792
    let (forum, _) = seeded(Floor::Forum);
793
    let response = send(&forum, "GET", "/runs/cafebabe", &[], "").await;
794
    assert!(advertised(&response).is_empty());
795
}
796
797
fn linked_rels(response: &Response) -> Vec<String> {
798
    header(response, "link")
799
        .split(", <")
800
        .filter_map(|entry| {
801
            entry
802
                .split("rels/")
803
                .nth(1)?
804
                .split('"')
805
                .next()
806
                .map(str::to_owned)
807
        })
808
        .filter(|rel| rel != "technique")
809
        .collect()
810
}
811
812
#[tokio::test]
813
async fn links_hide_resources_that_would_do_nothing() {
814
    let (bedroom, _) = seeded(Floor::Bedroom);
815
    let before = linked_rels(&send(&bedroom, "GET", "/runs/cafebabe", &[], "").await);
816
    assert!(before.contains(&"drawer".to_owned()));
817
    assert!(!before.contains(&"diagnosis".to_owned()));
818
    assert!(!before.contains(&"thread".to_owned()));
819
    let searched = send(&bedroom, "POST", "/runs/cafebabe/drawer", &[], "").await;
820
    assert!(!linked_rels(&searched).contains(&"drawer".to_owned()));
821
822
    let (clinic, _) = seeded(Floor::Clinic);
823
    let rels = linked_rels(&send(&clinic, "GET", "/runs/cafebabe", &[], "").await);
824
    assert!(rels.contains(&"diagnosis".to_owned()) && rels.contains(&"thread".to_owned()));
825
}
826
827
#[tokio::test]
828
async fn ignoring_mum_has_consequences() {
829
    let app = seeded_with(Floor::Bedroom, |run| {
830
        mum_at_the_door(run);
831
        run.here.turns = 6;
832
    });
833
    let knock = send(&app, "GET", "/runs/cafebabe", &[], "").await;
834
    assert_eq!(
835
        json(knock).await["type"],
836
        "https://jelq.dev/problems/someone-at-the-door"
837
    );
838
    let leave = send(&app, "POST", "/runs/cafebabe/floor", &[], "").await;
839
    assert_eq!(
840
        leave.status(),
841
        StatusCode::UNAUTHORIZED,
842
        "no leaving with Mum at the door"
843
    );
844
    send(&app, "GET", "/runs/cafebabe", &[], "").await;
845
    let opened = send(&app, "GET", "/runs/cafebabe", &[], "").await;
846
    assert_eq!(opened.status(), StatusCode::GONE);
847
    assert_eq!(
848
        json(opened).await["type"],
849
        "https://jelq.dev/problems/opened"
850
    );
851
}
852
853
#[tokio::test]
854
async fn caged_drawers_are_noisy_and_eventually_give_up_the_key() {
855
    let app = seeded_with(Floor::ChangingRoom, |run| run.items.cage = true);
856
    let mut statuses = Vec::new();
857
    for _ in 0..12 {
858
        let response = send(&app, "POST", "/runs/cafebabe/drawer", &[], "").await;
859
        statuses.push(response.status());
860
        if response.status() == StatusCode::OK {
861
            break;
862
        }
863
    }
864
    assert_eq!(statuses.last(), Some(&StatusCode::OK), "{statuses:?}");
865
    assert!(
866
        statuses[..statuses.len() - 1]
867
            .iter()
868
            .all(|s| *s == StatusCode::NO_CONTENT)
869
    );
870
}
871
872
#[tokio::test]
873
async fn the_ruler_replaces_the_self_report() {
874
    let app = seeded_with(Floor::Bedroom, |_| {});
875
    let before = send(&app, "GET", "/runs/cafebabe", &[], "").await;
876
    assert!(before.headers().contains_key("x-self-reported-length"));
877
    assert!(!before.headers().contains_key("x-length"));
878
879
    let app = seeded_with(Floor::Bedroom, |run| run.items.ruler = true);
880
    let after = send(&app, "GET", "/runs/cafebabe", &[], "").await;
881
    assert!(!after.headers().contains_key("x-self-reported-length"));
882
    assert!(after.headers().contains_key("x-length"));
883
}
884
885
#[tokio::test]
886
async fn an_open_tab_is_linked_and_jelq_still_answers_with_choices() {
887
    let app = seeded_with(Floor::Bathroom, |run| {
888
        run.story = Story::Pending;
889
        run.warmed_up = true;
890
    });
891
    let response = send(&app, "GET", "/runs/cafebabe", &[], "").await;
892
    let links = header(&response, "link");
893
    assert!(links.contains("/branches/forum>; rel=\"https://jelq.dev/rels/branch\""));
894
    assert!(links.contains("/branches/close-tab>; rel=\"https://jelq.dev/rels/branch\""));
895
    assert!(links.contains("?technique=wet>"));
896
    let jelq = send(&app, "JELQ", "/runs/cafebabe?technique=edge", &[], "").await;
897
    assert_eq!(jelq.status(), StatusCode::MULTIPLE_CHOICES);
898
}
899
900
#[tokio::test]
901
async fn either_story_choice_is_reachable_during_cooldown() {
902
    for choice in ["forum", "close-tab"] {
903
        let app = seeded_with(Floor::Bathroom, |run| {
904
            run.turn = 9;
905
            run.warmed_up = true;
906
            run.eq = 45;
907
            run.stamina = 1;
908
            run.items.door_locks = 1;
909
        });
910
        let tenth = send(&app, "JELQ", "/runs/cafebabe?technique=edge", &[], "").await;
911
        assert_eq!(tenth.status(), StatusCode::OK);
912
        let response = send(&app, "GET", "/runs/cafebabe", &[], "").await;
913
        assert!(!header(&response, "link").contains("?technique="));
914
        let path = format!("/runs/cafebabe/branches/{choice}");
915
        assert!(
916
            header(&response, "link")
917
                .contains(&format!("<{path}>; rel=\"https://jelq.dev/rels/branch\""))
918
        );
919
        let chosen = send(&app, "POST", &path, &[], "").await;
920
        assert_eq!(chosen.status(), StatusCode::SEE_OTHER);
921
        assert!(!header(&chosen, "link").contains("/branches/"));
922
        let repeated = send(&app, "POST", &path, &[], "").await;
923
        assert_eq!(repeated.status(), StatusCode::CONFLICT);
924
    }
925
}
926
927
#[tokio::test]
928
async fn a_door_lock_says_when_it_was_used() {
929
    let app = seeded_with(Floor::Bathroom, |run| {
930
        run.items.door_locks = 1;
931
        run.suspicion = SUSPICION_LIMIT;
932
        run.warmed_up = true;
933
    });
934
    let response = send(&app, "JELQ", "/runs/cafebabe?technique=dry", &[], "").await;
935
    assert_ne!(response.status(), StatusCode::CONFLICT);
936
    assert!(
937
        response
938
            .headers()
939
            .get_all("warning")
940
            .iter()
941
            .any(|w| w.to_str().unwrap().contains("your brother tried the door"))
942
    );
943
}
944
945
#[tokio::test]
946
async fn shopping_survives_browsing_but_not_jelqing() {
947
    let app = seeded_with(Floor::ChangingRoom, |run| run.cash = 50);
948
    send(&app, "GET", "/runs/cafebabe/shop", &[], "").await;
949
    let view = send(&app, "GET", "/runs/cafebabe", &[], "").await;
950
    assert!(header(&view, "link").contains("shop-item"));
951
    let jelq = send(&app, "JELQ", "/runs/cafebabe?technique=warmup", &[], "").await;
952
    assert_ne!(jelq.status(), StatusCode::METHOD_NOT_ALLOWED);
953
    let after = send(&app, "GET", "/runs/cafebabe", &[], "").await;
954
    assert!(!header(&after, "link").contains("shop-item"));
955
}
956
957
#[tokio::test]
958
async fn the_thread_announces_itself_at_the_branch() {
959
    let app = seeded_with(Floor::Bathroom, |run| {
960
        run.turn = 8;
961
        run.warmed_up = true;
962
        run.items.baby_oil = 9;
963
    });
964
    send(
965
        &app,
966
        "JELQ",
967
        "/runs/cafebabe?technique=wet",
968
        &[("content-type", "application/baby-oil")],
969
        "",
970
    )
971
    .await;
972
    let ninth = send(&app, "GET", "/runs/cafebabe", &[], "").await;
973
    assert!(!header(&ninth, "link").contains("/forum/threads/2009"));
974
    let tenth = send(
975
        &app,
976
        "JELQ",
977
        "/runs/cafebabe?technique=wet",
978
        &[("content-type", "application/baby-oil")],
979
        "",
980
    )
981
    .await;
982
    assert!(
983
        tenth
984
            .headers()
985
            .get_all("warning")
986
            .iter()
987
            .any(|w| w.to_str().unwrap().contains("a forum thread from 2009"))
988
    );
989
    assert!(header(&tenth, "link").contains("/forum/threads/2009"));
990
    let announced = send(&app, "GET", "/runs/cafebabe", &[], "").await;
991
    assert!(header(&announced, "link").contains("/forum/threads/2009"));
992
}
993
994
#[tokio::test]
995
async fn rereading_the_thread_teaches_uli() {
996
    let mut taught_runs = 0;
997
    let mut ruptured = 0;
998
    for seed in 0..100u64 {
999
        let app = app_with(Floor::Bedroom, seed, |run| {
1000
            run.turn = BRANCH_TURN;
1001
            run.warmed_up = true;
1002
            run.story = Story::ClosedTab;
1003
        });
1004
        let mut taught = false;
1005
        for _ in 0..40 {
1006
            let read = send(&app, "POST", "/runs/cafebabe/re-read", &[], "").await;
1007
            match read.status() {
1008
                StatusCode::NOT_MODIFIED => {
1009
                    assert!(header(&read, "warning").contains("2009"));
1010
                }
1011
                StatusCode::MULTIPLE_CHOICES => {
1012
                    send(&app, "POST", "/runs/cafebabe/branches/close-tab", &[], "").await;
1013
                }
1014
                StatusCode::OK => {
1015
                    assert!(header(&read, "location").contains("/techniques"));
1016
                    taught = true;
1017
                    break;
1018
                }
1019
                StatusCode::INTERNAL_SERVER_ERROR => break,
1020
                other => panic!("seed {seed} read the thread into {other}"),
1021
            }
1022
        }
1023
        if taught {
1024
            taught_runs += 1;
1025
            let jelq = send(&app, "JELQ", "/runs/cafebabe?technique=uli", &[], "").await;
1026
            assert_ne!(jelq.status(), StatusCode::FORBIDDEN);
1027
        }
1028
        ruptured += u32::from(!taught);
1029
    }
1030
    assert!(
1031
        taught_runs >= 85,
1032
        "{taught_runs}/100 learned uli, {ruptured} ruptured first"
1033
    );
1034
}
1035
1036
#[tokio::test]
1037
async fn meditation_is_free_but_only_once_per_floor() {
1038
    let app = seeded_with(Floor::Bedroom, |_| {});
1039
    let response = send(&app, "JELQ", "/runs/cafebabe?technique=meditate", &[], "").await;
1040
    assert_eq!(response.status(), StatusCode::OK);
1041
    assert_eq!(header(&response, "x-eq"), "10");
1042
    assert_eq!(header(&response, "x-stamina"), "10/10");
1043
    let again = send(&app, "JELQ", "/runs/cafebabe?technique=meditate", &[], "").await;
1044
    assert_eq!(again.status(), StatusCode::CONFLICT);
1045
}
1046
1047
#[tokio::test]
1048
async fn meditation_is_listed_from_the_start() {
1049
    let app = seeded_with(Floor::Bedroom, |_| {});
1050
    let view = send(&app, "GET", "/runs/cafebabe", &[], "").await;
1051
    assert!(header(&view, "link").contains("technique=meditate"));
1052
    let jelq = send(&app, "JELQ", "/runs/cafebabe?technique=meditate", &[], "").await;
1053
    assert_eq!(jelq.status(), StatusCode::OK);
1054
}
1055
1056
fn app_with(floor: Floor, seed: u64, setup: impl FnOnce(&mut Run)) -> Router {
1057
    let state = Arc::new(AppState::default());
1058
    let mut run = Run::new("cafebabe".into(), "cafe".into(), seed);
1059
    run.floor = floor;
1060
    setup(&mut run);
1061
    state.insert(run);
1062
    router(state)
1063
}
1064
1065
#[tokio::test]
1066
async fn hold_is_offered_only_at_the_edges() {
1067
    let calm = seeded_with(Floor::Bedroom, |_| {});
1068
    let refused = send(&calm, "HOLD", "/runs/cafebabe", &[], "").await;
1069
    assert_eq!(refused.status(), StatusCode::METHOD_NOT_ALLOWED);
1070
    assert!(!header(&refused, "allow").contains("HOLD"));
1071
    let view = send(&calm, "GET", "/runs/cafebabe", &[], "").await;
1072
    assert!(!header(&view, "link").contains("HOLD"));
1073
    let options = send(&calm, "OPTIONS", "/runs/cafebabe", &[], "").await;
1074
    assert!(!header(&options, "allow").contains("HOLD"));
1075
1076
    let winded = seeded_with(Floor::Bedroom, |run| run.stamina = 2);
1077
    let listed = send(&winded, "GET", "/runs/cafebabe", &[], "").await;
1078
    assert!(header(&listed, "link").contains("HOLD"));
1079
    let options = send(&winded, "OPTIONS", "/runs/cafebabe", &[], "").await;
1080
    assert!(header(&options, "allow").contains("HOLD"));
1081
1082
    let hot = seeded_with(Floor::Bedroom, |run| {
1083
        run.eq = 75;
1084
        run.warmed_up = true;
1085
    });
1086
    let held = send(&hot, "HOLD", "/runs/cafebabe", &[], "").await;
1087
    assert_eq!(held.status(), StatusCode::NO_CONTENT);
1088
    assert_eq!(header(&held, "x-stamina"), "10/10");
1089
}
1090
1091
#[tokio::test]
1092
async fn holding_calms_and_sometimes_reflects() {
1093
    let mut calmed = 0;
1094
    let mut reflected = 0;
1095
    let mut ruptured = 0;
1096
    for seed in 0..120u64 {
1097
        let mut run = Run::new("cafebabe".into(), "cafe".into(), seed);
1098
        run.eq = 75;
1099
        match run.hold(Instant::now()) {
1100
            Ok(_) if run.eq == 0 => reflected += 1,
1101
            Ok(_) => calmed += 1,
1102
            Err(_) => ruptured += 1,
1103
        }
1104
    }
1105
    assert!(
1106
        calmed > 100 && reflected > 0,
1107
        "one HOLD in twelve should reflect, got {calmed} calm and {reflected} reflective of 120"
1108
    );
1109
    assert!(
1110
        calmed + reflected + ruptured == 120 && ruptured <= 4,
1111
        "the flat 1 in 250 ruptured {ruptured} of 120 holds"
1112
    );
1113
}
1114
1115
#[tokio::test]
1116
async fn leaving_the_band_warms_the_next_floor() {
1117
    let app = seeded_with(Floor::Bedroom, |run| {
1118
        run.eq = 60;
1119
        run.warmed_up = true;
1120
        run.here.turns = 6;
1121
        run.items.baby_oil = 9;
1122
    });
1123
    let left = send(&app, "POST", "/runs/cafebabe/floor", &[], "").await;
1124
    assert_eq!(left.status(), StatusCode::SEE_OTHER);
1125
    let detail = json(left).await["detail"].as_str().unwrap().to_owned();
1126
    assert!(detail.contains("the warmth came with you"));
1127
1128
    let lube = [("content-type", "application/baby-oil")];
1129
    send(&app, "JELQ", "/runs/cafebabe?technique=warmup", &[], "").await;
1130
    // The shower is a +35 warm-up, so EQ lands in the band and the band bonus rides along.
1131
    // The arrival bonus is the one point the second jelq loses.
1132
    let first = send(&app, "JELQ", "/runs/cafebabe?technique=wet", &lube, "").await;
1133
    assert!(
1134
        header(&first, "server-timing").contains("d20+4"),
1135
        "the first jelq of a warm arrival rolls with +1, got {}",
1136
        header(&first, "server-timing")
1137
    );
1138
    let second = send(&app, "JELQ", "/runs/cafebabe?technique=wet", &lube, "").await;
1139
    assert!(header(&second, "server-timing").contains("d20+3"));
1140
}
1141
1142
#[tokio::test]
1143
async fn greed_multiplies_gains_above_the_band() {
1144
    let lube = [("content-type", "application/baby-oil")];
1145
    for seed in 0..60u64 {
1146
        let app = app_with(Floor::Bedroom, seed, |run| {
1147
            run.eq = 60;
1148
            run.warmed_up = true;
1149
        });
1150
        let in_band = send(&app, "JELQ", "/runs/cafebabe?technique=wet", &lube, "").await;
1151
        let app = app_with(Floor::Bedroom, seed, |run| {
1152
            run.eq = 75;
1153
            run.warmed_up = true;
1154
        });
1155
        let greedy = send(&app, "JELQ", "/runs/cafebabe?technique=wet", &lube, "").await;
1156
        if in_band.status() != StatusCode::OK || greedy.status() != StatusCode::OK {
1157
            continue;
1158
        }
1159
        let steady = json(in_band).await["length"].as_f64().unwrap();
1160
        let eager = json(greedy).await["length"].as_f64().unwrap();
1161
        assert!(
1162
            (eager - steady * 1.5).abs() < 1e-9,
1163
            "seed {seed} gained {eager} above the band against {steady} inside it"
1164
        );
1165
        return;
1166
    }
1167
    panic!("no seed in 60 produced two clean rolls to compare");
1168
}
1169
1170
#[tokio::test]
1171
async fn the_kitchen_is_only_on_the_home_floors() {
1172
    let absent = seeded_with(Floor::Bathroom, |_| {});
1173
    let no_kitchen = send(&absent, "POST", "/runs/cafebabe/kitchen", &[], "").await;
1174
    assert_eq!(no_kitchen.status(), StatusCode::NOT_FOUND);
1175
    let view = send(&absent, "GET", "/runs/cafebabe", &[], "").await;
1176
    assert!(!header(&view, "link").contains("/kitchen"));
1177
1178
    let app = seeded_with(Floor::Bedroom, |_| {});
1179
    let view = send(&app, "GET", "/runs/cafebabe", &[], "").await;
1180
    assert!(header(&view, "link").contains("/kitchen"));
1181
    let raid = send(&app, "POST", "/runs/cafebabe/kitchen", &[], "").await;
1182
    assert_ne!(raid.status(), StatusCode::NOT_FOUND);
1183
}
1184
1185
#[tokio::test]
1186
async fn raids_cost_the_turn_and_the_fridge_pays_out_sometimes() {
1187
    let mut bounties = std::collections::BTreeSet::new();
1188
    let mut told = 0;
1189
    for seed in 0..80u64 {
1190
        let mut run = Run::new("cafebabe".into(), "cafe".into(), seed);
1191
        let Ok(outcome) = run.raid_kitchen() else {
1192
            panic!("the first raid should never fail");
1193
        };
1194
        assert_eq!(run.turn, 1, "a raid costs the floor turn");
1195
        assert!(!run.here.melon_offered, "no melon at starting EQ");
1196
        if outcome.status == StatusCode::NO_CONTENT {
1197
            bounties.insert("empty");
1198
        } else {
1199
            bounties.insert("loot");
1200
        }
1201
        told += u32::from(run.kitchen_tellings);
1202
    }
1203
    assert!(bounties.contains("loot") && bounties.contains("empty"));
1204
    assert!(
1205
        (0..80).any(|seed| {
1206
            let mut run = Run::new("cafebabe".into(), "cafe".into(), seed);
1207
            run.raid_kitchen().is_ok() && run.kitchen_tellings == 0
1208
        }),
1209
        "most raids go unremarked"
1210
    );
1211
    assert!(told > 0, "someone should have been told across 80 raids");
1212
}
1213
1214
#[tokio::test]
1215
async fn the_watermelon_offers_itself_above_ninety() {
1216
    let mut offered = 0;
1217
    let mut caught = 0;
1218
    let mut survived = 0;
1219
    for seed in 0..120u64 {
1220
        let mut run = Run::new("cafebabe".into(), "cafe".into(), seed);
1221
        run.eq = 95;
1222
        let Ok(outcome) = run.raid_kitchen() else {
1223
            continue;
1224
        };
1225
        if outcome.status != StatusCode::MULTIPLE_CHOICES {
1226
            continue;
1227
        }
1228
        offered += 1;
1229
        let gains = run.length;
1230
        let disgrace = run.shame;
1231
        if run.commit_melon().is_err() {
1232
            caught += 1;
1233
            assert_eq!(run.ended.as_ref().unwrap().cause.slug(), "watermelon");
1234
        } else {
1235
            survived += 1;
1236
            assert_eq!(run.eq, 0);
1237
            assert_eq!(run.shame, disgrace + 5);
1238
            assert_eq!(run.length, gains, "the gains are kept");
1239
        }
1240
    }
1241
    assert!(
1242
        offered > 20,
1243
        "the melon should appear in two fifths of raids"
1244
    );
1245
    assert!(caught > 0 && survived > 0, "caught is one in three");
1246
}
1247
1248
#[tokio::test]
1249
async fn a_melon_offer_does_not_survive_a_non_kitchen_turn() {
1250
    for seed in 0..60u64 {
1251
        let jelq_app = app_with(Floor::Bedroom, seed, |run| {
1252
            run.warmed_up = true;
1253
            run.here.melon_offered = true;
1254
        });
1255
        let stroked = send(
1256
            &jelq_app,
1257
            "JELQ",
1258
            "/runs/cafebabe?technique=wet&intensity=2",
1259
            &[],
1260
            "",
1261
        )
1262
        .await;
1263
        if stroked.status() != StatusCode::OK && stroked.status() != StatusCode::PARTIAL_CONTENT {
1264
            continue;
1265
        }
1266
        let bite = send(
1267
            &jelq_app,
1268
            "POST",
1269
            "/runs/cafebabe/kitchen/watermelon",
1270
            &[],
1271
            "",
1272
        )
1273
        .await;
1274
        assert_eq!(
1275
            bite.status(),
1276
            StatusCode::CONFLICT,
1277
            "seed {seed} kept the melon through a jelq"
1278
        );
1279
1280
        let hold_app = app_with(Floor::Bedroom, seed, |run| {
1281
            run.stamina = 2;
1282
            run.here.melon_offered = true;
1283
        });
1284
        let held = send(&hold_app, "HOLD", "/runs/cafebabe", &[], "").await;
1285
        if held.status() != StatusCode::NO_CONTENT {
1286
            continue;
1287
        }
1288
        let bite = send(
1289
            &hold_app,
1290
            "POST",
1291
            "/runs/cafebabe/kitchen/watermelon",
1292
            &[],
1293
            "",
1294
        )
1295
        .await;
1296
        assert_eq!(
1297
            bite.status(),
1298
            StatusCode::CONFLICT,
1299
            "seed {seed} kept the melon through a hold"
1300
        );
1301
1302
        let read_app = app_with(Floor::Bedroom, seed, |run| {
1303
            run.story = Story::ClosedTab;
1304
            run.here.melon_offered = true;
1305
        });
1306
        let read = send(&read_app, "POST", "/runs/cafebabe/re-read", &[], "").await;
1307
        if read.status() == StatusCode::INTERNAL_SERVER_ERROR {
1308
            continue;
1309
        }
1310
        let bite = send(
1311
            &read_app,
1312
            "POST",
1313
            "/runs/cafebabe/kitchen/watermelon",
1314
            &[],
1315
            "",
1316
        )
1317
        .await;
1318
        assert_eq!(
1319
            bite.status(),
1320
            StatusCode::CONFLICT,
1321
            "seed {seed} kept the melon through a re-read"
1322
        );
1323
    }
1324
1325
    let mut stood = 0;
1326
    for seed in 0..60u64 {
1327
        let mut run = Run::new("cafebabe".into(), "cafe".into(), seed);
1328
        run.eq = 95;
1329
        run.here.melon_offered = true;
1330
        if run.raid_kitchen().is_err() {
1331
            continue;
1332
        }
1333
        stood += 1;
1334
        assert!(
1335
            run.here.melon_offered,
1336
            "seed {seed} lost the melon to another raid"
1337
        );
1338
    }
1339
    assert!(stood > 40, "raids at eq 95 rarely rupture");
1340
}
1341
1342
#[tokio::test]
1343
async fn the_kitchen_closes_after_two_tellings() {
1344
    let mut closed = None;
1345
    for seed in 0..300u64 {
1346
        let mut run = Run::new("cafebabe".into(), "cafe".into(), seed);
1347
        let first = run.raid_kitchen();
1348
        let second = run.raid_kitchen();
1349
        if second.is_err() && first.is_ok() {
1350
            assert_eq!(run.kitchen_tellings, 2);
1351
            let Err(third) = run.raid_kitchen() else {
1352
                panic!("the kitchen should be gone");
1353
            };
1354
            assert_eq!(third.status(), StatusCode::GONE);
1355
            closed = Some(seed);
1356
            break;
1357
        }
1358
    }
1359
    assert!(
1360
        closed.is_some(),
1361
        "two tellings should occur within 300 seeds"
1362
    );
1363
}
1364
1365
#[tokio::test]
1366
async fn lubes_run_out_but_spit_never_does() {
1367
    let app = seeded_with(Floor::Bedroom, |run| {
1368
        run.warmed_up = true;
1369
        run.items.baby_oil = 1;
1370
    });
1371
    let lube = [("content-type", "application/baby-oil")];
1372
    let first = send(&app, "JELQ", "/runs/cafebabe?technique=wet", &lube, "").await;
1373
    assert_ne!(first.status(), StatusCode::PRECONDITION_FAILED);
1374
    let second = send(&app, "JELQ", "/runs/cafebabe?technique=wet", &lube, "").await;
1375
    assert_eq!(second.status(), StatusCode::UNSUPPORTED_MEDIA_TYPE);
1376
1377
    let spit = [("content-type", "text/plain; charset=spit")];
1378
    for _ in 0..3 {
1379
        let wet = send(&app, "JELQ", "/runs/cafebabe?technique=wet", &spit, "").await;
1380
        assert_ne!(wet.status(), StatusCode::PRECONDITION_FAILED);
1381
    }
1382
}
1383
1384
#[tokio::test]
1385
async fn energy_drinks_add_three_stamina() {
1386
    let app = seeded_with(Floor::Bedroom, |run| {
1387
        run.stamina = 2;
1388
        run.items.energy_drinks = 1;
1389
    });
1390
    let view = send(&app, "GET", "/runs/cafebabe", &[], "").await;
1391
    assert!(header(&view, "link").contains("technique=energy"));
1392
    let drink = send(&app, "JELQ", "/runs/cafebabe?technique=energy", &[], "").await;
1393
    assert_eq!(drink.status(), StatusCode::OK);
1394
    let view = send(&app, "GET", "/runs/cafebabe", &[], "").await;
1395
    assert_eq!(header(&view, "x-stamina"), "5/10");
1396
    let again = send(&app, "JELQ", "/runs/cafebabe?technique=energy", &[], "").await;
1397
    assert_eq!(again.status(), StatusCode::FAILED_DEPENDENCY);
1398
}
1399
1400
#[tokio::test]
1401
async fn the_working_supplement_resets_stamina() {
1402
    let mut worked = 0;
1403
    let mut bunk = 0;
1404
    for seed in 0..60u64 {
1405
        let app = app_with(Floor::Bedroom, seed, |run| {
1406
            run.stamina = 1;
1407
            run.items.supplements = 1;
1408
        });
1409
        let taken = send(&app, "JELQ", "/runs/cafebabe?technique=supplement", &[], "").await;
1410
        if taken.status() == StatusCode::OK {
1411
            worked += 1;
1412
            let view = send(&app, "GET", "/runs/cafebabe", &[], "").await;
1413
            assert_eq!(header(&view, "x-stamina"), "10/10");
1414
        } else {
1415
            assert_eq!(taken.status(), StatusCode::BAD_GATEWAY);
1416
            bunk += 1;
1417
        }
1418
    }
1419
    assert!(worked > 0 && bunk > 0, "one in five should work too well");
1420
}
1421
1422
#[tokio::test]
1423
async fn dry_jelqs_build_friction_and_real_lube_does_not() {
1424
    let app = seeded_with(Floor::Bedroom, |run| {
1425
        run.warmed_up = true;
1426
    });
1427
    let dry = send(&app, "JELQ", "/runs/cafebabe?technique=dry", &[], "").await;
1428
    assert_eq!(header(&dry, "x-friction"), "2/10");
1429
    let lube = [("content-type", "application/baby-oil")];
1430
    let wet = send(
1431
        &app,
1432
        "JELQ",
1433
        "/runs/cafebabe?technique=wet&intensity=5",
1434
        &lube,
1435
        "",
1436
    )
1437
    .await;
1438
    assert_eq!(header(&wet, "x-friction"), "2/10");
1439
    let spit = [("content-type", "text/plain; charset=spit")];
1440
    let glisten = send(
1441
        &app,
1442
        "JELQ",
1443
        "/runs/cafebabe?technique=wet&intensity=5",
1444
        &spit,
1445
        "",
1446
    )
1447
    .await;
1448
    assert_eq!(header(&glisten, "x-friction"), "3/10");
1449
}
1450
1451
#[tokio::test]
1452
async fn switching_grips_makes_the_friction_cheaper() {
1453
    let app = seeded_with(Floor::Bedroom, |run| {
1454
        run.warmed_up = true;
1455
    });
1456
    let dry = send(&app, "JELQ", "/runs/cafebabe?technique=dry", &[], "").await;
1457
    assert_eq!(header(&dry, "x-friction"), "2/10");
1458
    send(
1459
        &app,
1460
        "GET",
1461
        "/runs/cafebabe/grip",
1462
        &[("upgrade", "underhand"), ("connection", "upgrade")],
1463
        "",
1464
    )
1465
    .await;
1466
    let switched = send(&app, "JELQ", "/runs/cafebabe?technique=dry", &[], "").await;
1467
    assert_eq!(header(&switched, "x-friction"), "3/10");
1468
    let settled = send(&app, "JELQ", "/runs/cafebabe?technique=dry", &[], "").await;
1469
    assert_eq!(header(&settled, "x-friction"), "5/10");
1470
}
1471
1472
#[tokio::test]
1473
async fn holding_and_meditating_shed_friction() {
1474
    let app = seeded_with(Floor::Bedroom, |run| {
1475
        run.eq = 80;
1476
        run.friction = 5;
1477
    });
1478
    let held = send(&app, "HOLD", "/runs/cafebabe", &[], "").await;
1479
    assert_eq!(held.status(), StatusCode::NO_CONTENT);
1480
    let view = send(&app, "GET", "/runs/cafebabe", &[], "").await;
1481
    assert_eq!(header(&view, "x-friction"), "3/10");
1482
1483
    let taught = seeded_with(Floor::Bedroom, |run| {
1484
        run.friction = 5;
1485
    });
1486
    let calmed = send(
1487
        &taught,
1488
        "JELQ",
1489
        "/runs/cafebabe?technique=meditate",
1490
        &[],
1491
        "",
1492
    )
1493
    .await;
1494
    assert_eq!(calmed.status(), StatusCode::OK);
1495
    let view = send(&taught, "GET", "/runs/cafebabe", &[], "").await;
1496
    assert_eq!(header(&view, "x-friction"), "3/10");
1497
}
1498
1499
#[tokio::test]
1500
async fn a_friction_burn_halves_the_gains() {
1501
    let lube = [("content-type", "application/baby-oil")];
1502
    let path = "/runs/cafebabe?technique=wet&intensity=5";
1503
    for seed in 0..60u64 {
1504
        let steady = app_with(Floor::Bedroom, seed, |run| {
1505
            run.warmed_up = true;
1506
            run.eq = 60;
1507
            run.items.baby_oil = 9;
1508
        });
1509
        let hurt_app = app_with(Floor::Bedroom, seed, |run| {
1510
            run.warmed_up = true;
1511
            run.eq = 60;
1512
            run.items.baby_oil = 9;
1513
            run.conditions.friction_burn = true;
1514
        });
1515
        let calm = send(&steady, "JELQ", path, &lube, "").await;
1516
        let hurt = send(&hurt_app, "JELQ", path, &lube, "").await;
1517
        if calm.status() != StatusCode::OK || hurt.status() != StatusCode::OK {
1518
            continue;
1519
        }
1520
        let calm_gain = json(calm).await["length"].as_f64().unwrap();
1521
        let hurt_gain = json(hurt).await["length"].as_f64().unwrap();
1522
        assert!(
1523
            (hurt_gain * 2.0 - calm_gain).abs() < 1e-9,
1524
            "seed {seed} burned {hurt_gain} against {calm_gain}"
1525
        );
1526
        return;
1527
    }
1528
    panic!("no seed in 60 produced two clean rolls to compare");
1529
}
1530
1531
#[tokio::test]
1532
async fn the_burn_warns_and_the_ice_treats_it() {
1533
    let app = seeded_with(Floor::Bedroom, |run| {
1534
        run.warmed_up = true;
1535
        run.eq = 60;
1536
        run.items.baby_oil = 9;
1537
        run.friction = 7;
1538
        run.conditions.friction_burn = true;
1539
    });
1540
    let lube = [("content-type", "application/baby-oil")];
1541
    let jelq = send(
1542
        &app,
1543
        "JELQ",
1544
        "/runs/cafebabe?technique=wet&intensity=5",
1545
        &lube,
1546
        "",
1547
    )
1548
    .await;
1549
    let warnings: Vec<_> = jelq
1550
        .headers()
1551
        .get_all("warning")
1552
        .iter()
1553
        .filter_map(|w| w.to_str().ok())
1554
        .collect();
1555
    assert!(warnings.contains(&"199 - \"friction burn\""));
1556
    send(&app, "JELQ", "/runs/cafebabe?technique=ice", &[], "").await;
1557
    let view = send(&app, "GET", "/runs/cafebabe", &[], "").await;
1558
    let warnings: Vec<_> = view
1559
        .headers()
1560
        .get_all("warning")
1561
        .iter()
1562
        .filter_map(|w| w.to_str().ok())
1563
        .collect();
1564
    assert!(!warnings.contains(&"199 - \"friction burn\""));
1565
}
1566
1567
#[tokio::test]
1568
async fn the_burn_shifts_the_rupture_tiers() {
1569
    async fn ruptures(burned: bool) -> u32 {
1570
        let mut gone = 0;
1571
        for seed in 0..400u64 {
1572
            let app = app_with(Floor::Bedroom, seed, |run| {
1573
                run.warmed_up = true;
1574
                run.eq = 60;
1575
                run.items.baby_oil = 9;
1576
                run.conditions.friction_burn = burned;
1577
            });
1578
            let lube = [("content-type", "application/baby-oil")];
1579
            let jelq = send(
1580
                &app,
1581
                "JELQ",
1582
                "/runs/cafebabe?technique=wet&intensity=5",
1583
                &lube,
1584
                "",
1585
            )
1586
            .await;
1587
            if jelq.status() == StatusCode::INTERNAL_SERVER_ERROR {
1588
                gone += 1;
1589
            }
1590
        }
1591
        gone
1592
    }
1593
    let burned = ruptures(true).await;
1594
    let calm = ruptures(false).await;
1595
    assert!(
1596
        burned >= 8 && calm <= 4,
1597
        "burned {burned} ruptures against {calm}"
1598
    );
1599
}
1600
1601
#[tokio::test]
1602
async fn meditation_respects_the_priapism_floor() {
1603
    let app = seeded_with(Floor::Bedroom, |run| {
1604
        run.eq = 80;
1605
        run.conditions.priapism_turns = 3;
1606
    });
1607
    let calmed = send(&app, "JELQ", "/runs/cafebabe?technique=meditate", &[], "").await;
1608
    assert_eq!(calmed.status(), StatusCode::OK);
1609
    let view = send(&app, "GET", "/runs/cafebabe", &[], "").await;
1610
    assert_eq!(header(&view, "x-eq"), "75");
1611
}
1612
1613
#[tokio::test]
1614
async fn the_kitchen_pays_out_at_the_documented_odds() {
1615
    let mut oil = 0;
1616
    let mut drinks = 0;
1617
    let mut packs = 0;
1618
    for seed in 0..600u64 {
1619
        let mut run = Run::new("cafebabe".into(), "cafe".into(), seed);
1620
        run.kitchen_tellings = 0;
1621
        let held = (
1622
            run.items.coconut_oil,
1623
            run.items.energy_drinks,
1624
            run.items.ice_packs,
1625
        );
1626
        let _ = run.raid_kitchen();
1627
        oil += u32::from(run.items.coconut_oil > held.0);
1628
        drinks += u32::from(run.items.energy_drinks > held.1);
1629
        packs += u32::from(run.items.ice_packs > held.2);
1630
    }
1631
    assert!((170..=230).contains(&oil), "coconut {oil} in 600");
1632
    assert!((90..=150).contains(&drinks), "energy {drinks} in 600");
1633
    assert!((45..=105).contains(&packs), "ice {packs} in 600");
1634
}
1635
1636
#[tokio::test]
1637
async fn a_refused_jelq_leaves_the_lube_alone() {
1638
    let app = seeded_with(Floor::Bedroom, |run| {
1639
        run.warmed_up = true;
1640
        run.items.extender = true;
1641
        run.girth = STORAGE_GIRTH + 1.0;
1642
    });
1643
    let lube = [("content-type", "application/baby-oil")];
1644
    let full = send(
1645
        &app,
1646
        "JELQ",
1647
        "/runs/cafebabe?technique=wet&intensity=5",
1648
        &lube,
1649
        "",
1650
    )
1651
    .await;
1652
    assert_eq!(full.status(), StatusCode::INSUFFICIENT_STORAGE);
1653
    let inv = send(&app, "GET", "/runs/cafebabe/inventory", &[], "").await;
1654
    let body = json(inv).await;
1655
    let held = body["items"].as_array().unwrap();
1656
    assert!(held.iter().any(|i| i["name"] == "baby-oil"));
1657
}
1658
1659
#[tokio::test]
1660
async fn an_empty_bottle_is_declined_before_the_enemy_stirs() {
1661
    let control = seeded_with(Floor::ChangingRoom, |run| {
1662
        run.warmed_up = true;
1663
        run.suspicion = SUSPICION_LIMIT;
1664
    });
1665
    let lube = [("content-type", "application/baby-oil")];
1666
    let caught = send(
1667
        &control,
1668
        "JELQ",
1669
        "/runs/cafebabe?technique=wet&intensity=5",
1670
        &lube,
1671
        "",
1672
    )
1673
    .await;
1674
    assert_eq!(caught.status(), StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS);
1675
1676
    let app = seeded_with(Floor::ChangingRoom, |run| {
1677
        run.warmed_up = true;
1678
        run.suspicion = SUSPICION_LIMIT;
1679
        run.items.baby_oil = 0;
1680
    });
1681
    let refused = send(
1682
        &app,
1683
        "JELQ",
1684
        "/runs/cafebabe?technique=wet&intensity=5",
1685
        &lube,
1686
        "",
1687
    )
1688
    .await;
1689
    assert_eq!(refused.status(), StatusCode::UNSUPPORTED_MEDIA_TYPE);
1690
    let view = send(&app, "GET", "/runs/cafebabe", &[], "").await;
1691
    assert_eq!(header(&view, "x-suspicion"), "5/5");
1692
    assert_eq!(header(&view, "x-shame"), "0");
1693
}
1694
1695
#[tokio::test]
1696
async fn the_top_tier_rolls_a_single_third() {
1697
    let mut ruptures = 0;
1698
    let mut early = 0;
1699
    for seed in 0..400u64 {
1700
        let app = app_with(Floor::Bedroom, seed, |run| {
1701
            run.warmed_up = true;
1702
            run.eq = 96;
1703
            run.items.baby_oil = 9;
1704
            run.items.blinds = true;
1705
        });
1706
        let lube = [("content-type", "application/baby-oil")];
1707
        let jelq = send(
1708
            &app,
1709
            "JELQ",
1710
            "/runs/cafebabe?technique=wet&intensity=5",
1711
            &lube,
1712
            "",
1713
        )
1714
        .await;
1715
        match jelq.status() {
1716
            StatusCode::INTERNAL_SERVER_ERROR => ruptures += 1,
1717
            StatusCode::TOO_EARLY => early += 1,
1718
            other => panic!("seed {seed} ended in {other}"),
1719
        }
1720
    }
1721
    assert!(
1722
        (90..=175).contains(&ruptures),
1723
        "ruptures {ruptures} of 400 against {early} early"
1724
    );
1725
    assert!(
1726
        (225..=310).contains(&early),
1727
        "early {early} of 400 against {ruptures} ruptures"
1728
    );
1729
}
1730
1731
#[tokio::test]
1732
async fn priapism_holds_the_floor_through_every_reset() {
1733
    let exit = seeded_with(Floor::Bedroom, |run| {
1734
        run.warmed_up = true;
1735
        run.eq = 90;
1736
        run.here.turns = 99;
1737
        run.conditions.priapism_turns = 3;
1738
    });
1739
    let out = send(&exit, "POST", "/runs/cafebabe/floor", &[], "").await;
1740
    assert_eq!(out.status(), StatusCode::SEE_OTHER);
1741
    let view = send(&exit, "GET", "/runs/cafebabe", &[], "").await;
1742
    assert_eq!(header(&view, "x-eq"), "75");
1743
1744
    let melon = seeded_with(Floor::Bedroom, |run| {
1745
        run.eq = 95;
1746
        run.here.melon_offered = true;
1747
        run.conditions.priapism_turns = 2;
1748
    });
1749
    let bite = send(&melon, "POST", "/runs/cafebabe/kitchen/watermelon", &[], "").await;
1750
    assert_eq!(bite.status(), StatusCode::OK);
1751
    let view = send(&melon, "GET", "/runs/cafebabe", &[], "").await;
1752
    assert_eq!(header(&view, "x-eq"), "75");
1753
}
1754
1755
#[tokio::test]
1756
async fn holding_rolls_a_flat_rupture_chance() {
1757
    async fn ruptures(eq: i32, stamina: i32) -> u32 {
1758
        let mut gone = 0;
1759
        for seed in 0..600u64 {
1760
            let app = app_with(Floor::Bedroom, seed, |run| {
1761
                run.warmed_up = true;
1762
                run.eq = eq;
1763
                run.stamina = stamina;
1764
            });
1765
            let held = send(&app, "HOLD", "/runs/cafebabe", &[], "").await;
1766
            if held.status() == StatusCode::INTERNAL_SERVER_ERROR {
1767
                gone += 1;
1768
            }
1769
        }
1770
        gone
1771
    }
1772
    let high = ruptures(90, 10).await;
1773
    let low = ruptures(30, 2).await;
1774
    assert!((0..=9).contains(&high), "high holds ruptured {high}/600");
1775
    assert!((0..=9).contains(&low), "low holds ruptured {low}/600");
1776
}
1777
1778
#[tokio::test]
1779
async fn the_kitchen_rolls_the_normal_tiers() {
1780
    async fn ruptures(eq: i32) -> u32 {
1781
        let mut gone = 0;
1782
        for seed in 0..600u64 {
1783
            let app = app_with(Floor::Bedroom, seed, |run| run.eq = eq);
1784
            let raid = send(&app, "POST", "/runs/cafebabe/kitchen", &[], "").await;
1785
            if raid.status() == StatusCode::INTERNAL_SERVER_ERROR {
1786
                gone += 1;
1787
            }
1788
        }
1789
        gone
1790
    }
1791
    let hot = ruptures(96).await;
1792
    let cool = ruptures(40).await;
1793
    assert!((40..=80).contains(&hot), "hot kitchen ruptured {hot}/600");
1794
    assert!((0..=9).contains(&cool), "cool kitchen ruptured {cool}/600");
1795
}
1796
1797
#[tokio::test]
1798
async fn the_thread_cannot_be_reread_before_anyone_mentions_it() {
1799
    let app = seeded_with(Floor::Bedroom, |run| run.warmed_up = true);
1800
    let read = send(&app, "POST", "/runs/cafebabe/re-read", &[], "").await;
1801
    assert_eq!(read.status(), StatusCode::NOT_FOUND);
1802
    let after = send(&app, "GET", "/runs/cafebabe", &[], "").await;
1803
    assert_eq!(header(&after, "x-floor"), "1");
1804
    assert!(!header(&after, "link").contains("/re-read"));
1805
}
1806
1807
#[tokio::test]
1808
async fn any_other_action_takes_the_melon_off_the_table() {
1809
    for (method, uri) in [
1810
        ("JELQ", "/runs/cafebabe?technique=warmup"),
1811
        ("POST", "/runs/cafebabe/drawer"),
1812
    ] {
1813
        let app = seeded_with(Floor::Bedroom, |run| {
1814
            run.eq = 95;
1815
            run.here.melon_offered = true;
1816
        });
1817
        send(&app, method, uri, &[], "").await;
1818
        let melon = send(&app, "POST", "/runs/cafebabe/kitchen/watermelon", &[], "").await;
1819
        assert_eq!(melon.status(), StatusCode::CONFLICT, "{method} {uri}");
1820
    }
1821
}
1822
1823
#[tokio::test]
1824
async fn a_refused_jelq_does_not_break_the_edging_streak() {
1825
    let app = seeded_with(Floor::ChangingRoom, |run| {
1826
        run.warmed_up = true;
1827
        run.eq = 45;
1828
        run.here.edges = 2;
1829
    });
1830
    let refused = send(
1831
        &app,
1832
        "JELQ",
1833
        "/runs/cafebabe?technique=dry",
1834
        &[("content-type", "application/baby-oil")],
1835
        "",
1836
    )
1837
    .await;
1838
    assert_eq!(refused.status(), StatusCode::UNSUPPORTED_MEDIA_TYPE);
1839
    let third = send(&app, "JELQ", "/runs/cafebabe?technique=edge", &[], "").await;
1840
    assert_eq!(third.status(), StatusCode::LOOP_DETECTED);
1841
}
1842
1843
#[tokio::test]
1844
async fn an_interrupted_edge_keeps_no_gains() {
1845
    let app = seeded_with(Floor::ChangingRoom, |run| {
1846
        run.warmed_up = true;
1847
        run.eq = 10;
1848
    });
1849
    let before = send(&app, "GET", "/runs/cafebabe", &[], "").await;
1850
    let girth = header(&before, "x-girth").to_owned();
1851
    let edge = send(&app, "JELQ", "/runs/cafebabe?technique=edge", &[], "").await;
1852
    assert_eq!(edge.status(), StatusCode::NOT_FOUND);
1853
    assert_eq!(header(&edge, "x-girth"), girth);
1854
}
1855
1856
#[tokio::test]
1857
async fn rummaging_for_the_key_leaves_the_drawer_unsearched() {
1858
    let mut found = 0;
1859
    for seed in 0..20u64 {
1860
        let app = app_with(Floor::Hotel, seed, |run| {
1861
            run.here.wifi_accepted = true;
1862
            run.items.cage = true;
1863
        });
1864
        for _ in 0..30 {
1865
            let rummage = send(&app, "POST", "/runs/cafebabe/drawer", &[], "").await;
1866
            if rummage.status() == StatusCode::OK {
1867
                break;
1868
            }
1869
        }
1870
        let search = send(&app, "POST", "/runs/cafebabe/drawer", &[], "").await;
1871
        if search.status() == StatusCode::FOUND {
1872
            assert!(header(&search, "location").ends_with("/inventory/do-not-disturb-sign"));
1873
            found += 1;
1874
        }
1875
    }
1876
    assert!(
1877
        found >= 18,
1878
        "only {found} of 20 hotel drawers survived the rummage"
1879
    );
1880
}
1881
1882
#[tokio::test]
1883
async fn a_full_server_buries_the_dead_to_make_room() {
1884
    let app = crate::app(Config {
1885
        max_runs: 1,
1886
        ..Config::default()
1887
    });
1888
    let first = send(&app, "POST", "/runs", &[], "").await;
1889
    let location = header(&first, "location").to_owned();
1890
    let full = send(&app, "POST", "/runs", &[], "").await;
1891
    assert_eq!(full.status(), StatusCode::SERVICE_UNAVAILABLE);
1892
    send(&app, "DELETE", &location, &[], "").await;
1893
    let second = send(&app, "POST", "/runs", &[], "").await;
1894
    assert_eq!(second.status(), StatusCode::CREATED);
1895
}