atagen

jelq

public

an http based roguelike rpg about jelqing

Star0
Watch0
7d3fb63208bf5064f04782aac0c0273305f8f35f
1
ssh://brewery@pls.lobotomise.me:33/atagen/jelq.git
src/tests.rsRust948 lines30.7 KB
1
use std::{sync::Arc, time::Duration};
2
3
use axum::{
4
    Router,
5
    body::{Body, to_bytes},
6
    http::{Method, Request, StatusCode, Version, header},
7
    response::Response,
8
};
9
use serde_json::Value;
10
use tower::ServiceExt;
11
12
use crate::{
13
    AppState, Config,
14
    game::{Floor, MumVisit, Run, Story, balance::SUSPICION_LIMIT},
15
    router,
16
};
17
18
async fn send(
19
    app: &Router,
20
    method: &str,
21
    uri: &str,
22
    headers: &[(&str, &str)],
23
    body: &str,
24
) -> Response {
25
    let mut request = Request::builder()
26
        .method(Method::from_bytes(method.as_bytes()).unwrap())
27
        .uri(uri);
28
    for (name, value) in headers {
29
        request = request.header(*name, *value);
30
    }
31
    app.clone()
32
        .oneshot(request.body(Body::from(body.to_owned())).unwrap())
33
        .await
34
        .unwrap()
35
}
36
37
async fn json(response: Response) -> Value {
38
    serde_json::from_slice(&to_bytes(response.into_body(), usize::MAX).await.unwrap()).unwrap()
39
}
40
41
async fn text(response: Response) -> String {
42
    String::from_utf8(
43
        to_bytes(response.into_body(), usize::MAX)
44
            .await
45
            .unwrap()
46
            .to_vec(),
47
    )
48
    .unwrap()
49
}
50
51
fn header<'a>(response: &'a Response, name: &str) -> &'a str {
52
    response.headers()[name].to_str().unwrap()
53
}
54
55
fn fixture() -> Run {
56
    Run::new("cafebabe".into(), "cafe".into(), 69)
57
}
58
59
fn seeded_with(floor: Floor, setup: impl FnOnce(&mut Run)) -> Router {
60
    let state = Arc::new(AppState::default());
61
    let mut run = fixture();
62
    run.floor = floor;
63
    setup(&mut run);
64
    state.insert(run);
65
    router(state)
66
}
67
68
fn seeded(floor: Floor) -> (Router, String) {
69
    let state = Arc::new(AppState::default());
70
    let mut run = fixture();
71
    run.floor = floor;
72
    let etag = run.origin_etag();
73
    state.insert(run);
74
    (router(state), etag)
75
}
76
77
fn app() -> Router {
78
    crate::app(Config::default())
79
}
80
81
fn mum_at_the_door(run: &mut Run) {
82
    run.mum = Some(MumVisit {
83
        errand: "She heard a noise.",
84
        nonce: "abc".into(),
85
        strikes: 0,
86
        knocked: false,
87
    });
88
}
89
90
#[tokio::test]
91
async fn creating_a_run_returns_location_and_etag() {
92
    let response = send(&app(), "POST", "/runs?seed=1", &[], "").await;
93
    assert_eq!(response.status(), StatusCode::CREATED);
94
    assert!(header(&response, "location").starts_with("/runs/"));
95
    assert!(response.headers().contains_key(header::ETAG));
96
    assert_eq!(header(&response, "server"), "jelqd/0.6.9");
97
}
98
99
#[tokio::test]
100
async fn bodies_do_not_repeat_the_headers() {
101
    let (app, _) = seeded(Floor::Bedroom);
102
    let response = send(&app, "GET", "/runs/cafebabe", &[], "").await;
103
    assert!(response.headers().contains_key("x-eq"));
104
    let body = json(response).await;
105
    for stat in [
106
        "eq",
107
        "girth",
108
        "foreskin",
109
        "stamina",
110
        "cash",
111
        "self_reported_length",
112
    ] {
113
        assert!(body.get(stat).is_none(), "{stat} is already a header");
114
    }
115
}
116
117
#[tokio::test]
118
async fn jelqing_cold_fails_the_precondition() {
119
    let (app, _) = seeded(Floor::Bedroom);
120
    let response = send(&app, "JELQ", "/runs/cafebabe?technique=stretch", &[], "").await;
121
    assert_eq!(response.status(), StatusCode::PRECONDITION_FAILED);
122
    assert_eq!(
123
        header(&response, "content-type"),
124
        "application/problem+json"
125
    );
126
}
127
128
#[tokio::test]
129
async fn wet_without_lube_is_unsupported_media() {
130
    let (app, _) = seeded(Floor::Bedroom);
131
    send(&app, "JELQ", "/runs/cafebabe?technique=warmup", &[], "").await;
132
    let response = send(&app, "JELQ", "/runs/cafebabe?technique=wet", &[], "").await;
133
    assert_eq!(response.status(), StatusCode::UNSUPPORTED_MEDIA_TYPE);
134
    let images = send(
135
        &app,
136
        "JELQ",
137
        "/runs/cafebabe?technique=wet",
138
        &[("content-type", "image/jpeg")],
139
        "",
140
    )
141
    .await;
142
    assert_eq!(json(images).await["detail"], "No.");
143
}
144
145
#[tokio::test]
146
async fn unknown_methods_are_not_implemented() {
147
    let (app, _) = seeded(Floor::Bedroom);
148
    let response = send(&app, "MILK", "/runs/cafebabe", &[], "").await;
149
    assert_eq!(response.status(), StatusCode::NOT_IMPLEMENTED);
150
    let teapot = send(&app, "BREW", "/runs/cafebabe", &[], "").await;
151
    assert_eq!(teapot.status(), StatusCode::IM_A_TEAPOT);
152
}
153
154
#[tokio::test]
155
async fn the_clinic_does_not_advertise_jelq() {
156
    let (app, _) = seeded(Floor::Clinic);
157
    let options = send(&app, "OPTIONS", "/runs/cafebabe", &[], "").await;
158
    assert!(!header(&options, "allow").contains("JELQ"));
159
    let jelq = send(&app, "JELQ", "/runs/cafebabe?technique=wet", &[], "").await;
160
    assert_eq!(jelq.status(), StatusCode::METHOD_NOT_ALLOWED);
161
    assert!(!header(&jelq, "allow").contains("JELQ"));
162
    let (bedroom, _) = seeded(Floor::Bedroom);
163
    let options = send(&bedroom, "OPTIONS", "/runs/cafebabe", &[], "").await;
164
    assert!(header(&options, "allow").contains("JELQ"));
165
}
166
167
#[tokio::test]
168
async fn patch_only_accepts_the_truth() {
169
    let (app, _) = seeded(Floor::Bedroom);
170
    let patch = [("content-type", "application/json-patch+json")];
171
    let replace = send(
172
        &app,
173
        "PATCH",
174
        "/runs/cafebabe",
175
        &patch,
176
        r#"[{"op":"replace","path":"/length","value":12}]"#,
177
    )
178
    .await;
179
    assert_eq!(replace.status(), StatusCode::UNPROCESSABLE_ENTITY);
180
    let lie = send(
181
        &app,
182
        "PATCH",
183
        "/runs/cafebabe",
184
        &patch,
185
        r#"[{"op":"test","path":"/length","value":40}]"#,
186
    )
187
    .await;
188
    assert_eq!(
189
        json(lie).await["type"],
190
        "https://jelq.dev/problems/exaggeration"
191
    );
192
}
193
194
#[tokio::test]
195
async fn deleting_the_foreskin_requires_certainty() {
196
    let (app, _) = seeded(Floor::Bedroom);
197
    let response = send(&app, "DELETE", "/runs/cafebabe/foreskin", &[], "").await;
198
    assert_eq!(response.status(), StatusCode::PRECONDITION_REQUIRED);
199
    let look = send(&app, "GET", "/runs/cafebabe/foreskin", &[], "").await;
200
    assert_eq!(look.status(), StatusCode::OK);
201
    let etag = header(&look, "etag").to_owned();
202
    assert!(text(look).await.contains('~'));
203
    let response = send(
204
        &app,
205
        "DELETE",
206
        "/runs/cafebabe/foreskin",
207
        &[("if-match", &etag)],
208
        "",
209
    )
210
    .await;
211
    assert_eq!(response.status(), StatusCode::NO_CONTENT);
212
    let response = send(&app, "GET", "/runs/cafebabe/foreskin", &[], "").await;
213
    assert_eq!(response.status(), StatusCode::GONE);
214
}
215
216
#[tokio::test]
217
async fn length_is_chunked_until_the_ruler() {
218
    let (app, _) = seeded(Floor::Bedroom);
219
    let lie = send(&app, "GET", "/runs/cafebabe/length", &[], "").await;
220
    assert!(!lie.headers().contains_key(header::CONTENT_LENGTH));
221
    let app = seeded_with(Floor::Bedroom, |run| run.items.ruler = true);
222
    let truth = send(&app, "GET", "/runs/cafebabe/length", &[], "").await;
223
    let declared: usize = header(&truth, "content-length").parse().unwrap();
224
    let body = text(truth).await;
225
    assert_eq!(body.len(), declared);
226
    assert!(body.starts_with("8=") && body.ends_with('D'));
227
}
228
229
#[tokio::test]
230
async fn length_supports_inch_ranges() {
231
    let (app, _) = seeded(Floor::Bedroom);
232
    let response = send(
233
        &app,
234
        "GET",
235
        "/runs/cafebabe/length",
236
        &[("range", "inches=0-1")],
237
        "",
238
    )
239
    .await;
240
    assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT);
241
    assert!(header(&response, "content-range").starts_with("inches 0.00-1.00/"));
242
    assert_eq!(text(response).await.len(), 25);
243
    let absurd = send(
244
        &app,
245
        "GET",
246
        "/runs/cafebabe/length",
247
        &[("range", "inches=40-50")],
248
        "",
249
    )
250
    .await;
251
    assert_eq!(absurd.status(), StatusCode::RANGE_NOT_SATISFIABLE);
252
    let malformed = send(
253
        &app,
254
        "GET",
255
        "/runs/cafebabe/length",
256
        &[("range", "inches=6.17")],
257
        "",
258
    )
259
    .await;
260
    assert_eq!(malformed.status(), StatusCode::OK);
261
    let suffix = send(
262
        &app,
263
        "GET",
264
        "/runs/cafebabe/length",
265
        &[("range", "inches=-1")],
266
        "",
267
    )
268
    .await;
269
    assert_eq!(suffix.status(), StatusCode::PARTIAL_CONTENT);
270
    assert!(text(suffix).await.ends_with('D'));
271
    for (range, status) in [
272
        ("inches=1-", StatusCode::PARTIAL_CONTENT),
273
        ("inches=40-", StatusCode::RANGE_NOT_SATISFIABLE),
274
        ("inches=-0", StatusCode::RANGE_NOT_SATISFIABLE),
275
        ("inches=2-1", StatusCode::OK),
276
        ("inches=NaN-2", StatusCode::OK),
277
        ("inches=0-inf", StatusCode::OK),
278
    ] {
279
        let response = send(
280
            &app,
281
            "GET",
282
            "/runs/cafebabe/length",
283
            &[("range", range)],
284
            "",
285
        )
286
        .await;
287
        assert_eq!(response.status(), status, "{range}");
288
    }
289
}
290
291
#[tokio::test]
292
async fn the_clinic_escalates_toward_honesty() {
293
    let (app, _) = seeded(Floor::Clinic);
294
    let mut hints = Vec::new();
295
    for _ in 0..3 {
296
        let response = send(
297
            &app,
298
            "GET",
299
            "/runs/cafebabe/diagnosis",
300
            &[("authorization", "Excuse \"changing\"")],
301
            "",
302
        )
303
        .await;
304
        assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
305
        hints.push(header(&response, "www-authenticate").to_owned());
306
    }
307
    assert!(hints[2].contains("Be honest."));
308
}
309
310
#[tokio::test]
311
async fn the_true_ending_is_not_modified() {
312
    let (early, _) = seeded(Floor::Bedroom);
313
    let response = send(&early, "GET", "/runs/cafebabe/diagnosis", &[], "").await;
314
    assert_eq!(response.status(), StatusCode::TOO_EARLY);
315
316
    let (app, etag) = seeded(Floor::Clinic);
317
    assert!(
318
        !etag.contains("cafebabe"),
319
        "the origin ETag must not be derivable from the id"
320
    );
321
    let response = send(
322
        &app,
323
        "GET",
324
        "/runs/cafebabe/diagnosis",
325
        &[
326
            ("authorization", "Bearer honesty"),
327
            ("if-none-match", &etag),
328
        ],
329
        "",
330
    )
331
    .await;
332
    assert_eq!(response.status(), StatusCode::NOT_MODIFIED);
333
    assert_eq!(header(&response, "x-diagnosis"), "normal");
334
}
335
336
#[tokio::test]
337
async fn mum_accepts_a_known_excuse() {
338
    let app = seeded_with(Floor::Bedroom, mum_at_the_door);
339
    let response = send(&app, "GET", "/runs/cafebabe", &[], "").await;
340
    assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
341
    assert_eq!(header(&response, "via"), "1.1 mum");
342
    let response = send(
343
        &app,
344
        "GET",
345
        "/runs/cafebabe",
346
        &[("authorization", "Excuse \"homework\"")],
347
        "",
348
    )
349
    .await;
350
    assert_eq!(response.status(), StatusCode::OK);
351
}
352
353
#[tokio::test]
354
async fn mum_runs_out_of_patience() {
355
    let app = seeded_with(Floor::Bedroom, mum_at_the_door);
356
    let lie = [("authorization", "Excuse \"it is for science\"")];
357
    for _ in 0..2 {
358
        let response = send(&app, "GET", "/runs/cafebabe", &lie, "").await;
359
        assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
360
    }
361
    let caught = send(&app, "GET", "/runs/cafebabe", &lie, "").await;
362
    assert_eq!(caught.status(), StatusCode::GONE);
363
    assert_eq!(
364
        json(caught).await["type"],
365
        "https://jelq.dev/problems/caught"
366
    );
367
}
368
369
#[tokio::test]
370
async fn mum_remembers_excuses_by_christmas() {
371
    let app = seeded_with(Floor::Nans, |run| {
372
        mum_at_the_door(run);
373
        run.heard.push("homework".into());
374
    });
375
    let basic = send(
376
        &app,
377
        "GET",
378
        "/runs/cafebabe",
379
        &[("authorization", "Excuse \"changing\"")],
380
        "",
381
    )
382
    .await;
383
    assert_eq!(basic.status(), StatusCode::UNAUTHORIZED);
384
    assert!(header(&basic, "www-authenticate").starts_with("Digest"));
385
    let replay = send(
386
        &app,
387
        "GET",
388
        "/runs/cafebabe",
389
        &[(
390
            "authorization",
391
            "Digest username=\"homework\", nonce=\"abc\"",
392
        )],
393
        "",
394
    )
395
    .await;
396
    assert_eq!(
397
        json(replay).await["type"],
398
        "https://jelq.dev/problems/excuse-replayed"
399
    );
400
    let fresh = send(
401
        &app,
402
        "GET",
403
        "/runs/cafebabe",
404
        &[(
405
            "authorization",
406
            "Digest username=\"wrapping presents\", nonce=\"abc\"",
407
        )],
408
        "",
409
    )
410
    .await;
411
    assert_eq!(fresh.status(), StatusCode::OK);
412
}
413
414
#[tokio::test]
415
async fn a_pulled_muscle_is_a_true_excuse() {
416
    let app = seeded_with(Floor::Nans, |run| {
417
        mum_at_the_door(run);
418
        run.conditions.pulled_muscle = true;
419
        run.heard.push("pulled a muscle".into());
420
    });
421
    let response = send(
422
        &app,
423
        "GET",
424
        "/runs/cafebabe",
425
        &[(
426
            "authorization",
427
            "Digest username=\"pulled a muscle\", nonce=\"abc\"",
428
        )],
429
        "",
430
    )
431
    .await;
432
    assert_eq!(response.status(), StatusCode::OK);
433
    assert!(header(&response, "x-stamina").ends_with("/7"));
434
}
435
436
#[tokio::test]
437
async fn floors_end_when_they_end() {
438
    let app = seeded_with(Floor::Bedroom, |_| {});
439
    let early = send(&app, "POST", "/runs/cafebabe/floor", &[], "").await;
440
    assert_eq!(early.status(), StatusCode::TOO_EARLY);
441
    let app = seeded_with(Floor::Bedroom, |run| run.here.turns = 6);
442
    let next = send(&app, "POST", "/runs/cafebabe/floor", &[], "").await;
443
    assert!(
444
        next.status() == StatusCode::SEE_OTHER || next.status() == StatusCode::CONFLICT,
445
        "either the floor changed or the zip did"
446
    );
447
}
448
449
#[tokio::test]
450
async fn pump_bro_upsells() {
451
    let closed = seeded_with(Floor::Bedroom, |_| {});
452
    let response = send(&closed, "GET", "/runs/cafebabe/shop", &[], "").await;
453
    assert_eq!(response.status(), StatusCode::NOT_FOUND);
454
455
    let app = seeded_with(Floor::ChangingRoom, |_| {});
456
    let pump = send(&app, "POST", "/runs/cafebabe/shop/pump", &[], "").await;
457
    assert_eq!(pump.status(), StatusCode::UPGRADE_REQUIRED);
458
    assert_eq!(header(&pump, "upgrade"), "pump-pro/2.0");
459
    let pro = send(&app, "POST", "/runs/cafebabe/shop/pump-pro", &[], "").await;
460
    assert_eq!(pro.status(), StatusCode::CREATED);
461
    let broke = send(&app, "POST", "/runs/cafebabe/shop/extender", &[], "").await;
462
    assert_eq!(broke.status(), StatusCode::PAYMENT_REQUIRED);
463
}
464
465
#[tokio::test]
466
async fn only_the_key_opens_the_cage() {
467
    let app = seeded_with(Floor::ChangingRoom, |run| run.items.cage = true);
468
    send(&app, "POST", "/runs/cafebabe/shop/ice-pack", &[], "").await;
469
    let still = send(
470
        &app,
471
        "GET",
472
        "/runs/cafebabe/inventory/chastity-cage",
473
        &[],
474
        "",
475
    )
476
    .await;
477
    assert_eq!(still.status(), StatusCode::OK);
478
    send(&app, "POST", "/runs/cafebabe/shop/cage-key", &[], "").await;
479
    let gone = send(
480
        &app,
481
        "GET",
482
        "/runs/cafebabe/inventory/chastity-cage",
483
        &[],
484
        "",
485
    )
486
    .await;
487
    assert_eq!(gone.status(), StatusCode::NOT_FOUND);
488
}
489
490
#[tokio::test]
491
async fn combos_are_multi_status_xml() {
492
    let app = seeded_with(Floor::Bathroom, |run| {
493
        run.foreskin = Some(1.0);
494
        run.items.blinds = true;
495
    });
496
    send(&app, "JELQ", "/runs/cafebabe?technique=warmup", &[], "").await;
497
    let response = send(
498
        &app,
499
        "JELQ",
500
        "/runs/cafebabe?technique=wet,stretch",
501
        &[("content-type", "application/baby-oil")],
502
        "",
503
    )
504
    .await;
505
    assert_eq!(response.status(), StatusCode::MULTI_STATUS);
506
    assert!(text(response).await.contains("<D:multistatus"));
507
}
508
509
#[tokio::test]
510
async fn the_elder_is_stale() {
511
    let app = seeded_with(Floor::Forum, |run| run.length = 5.0);
512
    let unverified = send(&app, "GET", "/runs/cafebabe/elder", &[], "").await;
513
    assert_eq!(
514
        unverified.status(),
515
        StatusCode::PROXY_AUTHENTICATION_REQUIRED
516
    );
517
    let lie = send(
518
        &app,
519
        "GET",
520
        "/runs/cafebabe/elder",
521
        &[("proxy-authorization", "Ruler 9")],
522
        "",
523
    )
524
    .await;
525
    assert_eq!(lie.status(), StatusCode::BAD_REQUEST);
526
    let csv = send(
527
        &app,
528
        "GET",
529
        "/runs/cafebabe/elder",
530
        &[("proxy-authorization", "Ruler 5.0")],
531
        "",
532
    )
533
    .await;
534
    assert_eq!(csv.status(), StatusCode::OK);
535
    let stale = send(
536
        &app,
537
        "GET",
538
        "/runs/cafebabe/elder",
539
        &[("if-modified-since", "Mon, 15 Jun 2009 00:00:00 GMT")],
540
        "",
541
    )
542
    .await;
543
    assert_eq!(stale.status(), StatusCode::NOT_MODIFIED);
544
}
545
546
#[tokio::test]
547
async fn the_displayed_length_is_accepted_as_honest() {
548
    let app = seeded_with(Floor::Forum, |run| run.length = 5.2496);
549
    let response = send(
550
        &app,
551
        "GET",
552
        "/runs/cafebabe/elder",
553
        &[("proxy-authorization", "Ruler 5.25")],
554
        "",
555
    )
556
    .await;
557
    assert_eq!(response.status(), StatusCode::OK);
558
}
559
560
#[tokio::test]
561
async fn the_hotel_has_a_captive_portal() {
562
    let app = seeded_with(Floor::Hotel, |_| {});
563
    let blocked = send(&app, "GET", "/runs/cafebabe", &[], "").await;
564
    assert_eq!(
565
        blocked.status(),
566
        StatusCode::NETWORK_AUTHENTICATION_REQUIRED
567
    );
568
    let accepted = send(&app, "POST", "/runs/cafebabe/portal", &[], "").await;
569
    assert_eq!(accepted.status(), StatusCode::SEE_OTHER);
570
    let online = send(&app, "GET", "/runs/cafebabe", &[], "").await;
571
    assert_eq!(online.status(), StatusCode::OK);
572
}
573
574
#[tokio::test]
575
async fn the_internet_never_forgets() {
576
    let app = seeded_with(Floor::Bedroom, |run| {
577
        run.filmed_at = Some(std::time::SystemTime::now());
578
    });
579
    let footage = send(&app, "GET", "/runs/cafebabe/footage", &[], "").await;
580
    assert_eq!(footage.status(), StatusCode::OK);
581
    assert_eq!(
582
        header(&footage, "cache-control"),
583
        "public, max-age=31536000, immutable"
584
    );
585
    assert!(footage.headers().contains_key(header::AGE));
586
    let delete = send(&app, "DELETE", "/runs/cafebabe/footage", &[], "").await;
587
    assert_eq!(delete.status(), StatusCode::METHOD_NOT_ALLOWED);
588
    assert!(!header(&delete, "allow").contains("DELETE"));
589
}
590
591
#[tokio::test]
592
async fn forged_ruler_cookies_are_lies() {
593
    let forged = send(
594
        &app(),
595
        "GET",
596
        "/leaderboard?sort=content-length",
597
        &[("cookie", "ruler=1")],
598
        "",
599
    )
600
    .await;
601
    assert_eq!(forged.status(), StatusCode::BAD_REQUEST);
602
}
603
604
#[tokio::test]
605
async fn listings_never_reveal_the_private_id() {
606
    let app = app();
607
    let created = send(&app, "POST", "/runs", &[], "").await;
608
    let location = header(&created, "location").to_owned();
609
    let board = text(send(&app, "GET", "/leaderboard", &[], "").await).await;
610
    let private = location.trim_start_matches("/runs/");
611
    assert!(!board.contains(private));
612
    let entry: Value = serde_json::from_str(&board).unwrap();
613
    let public = entry["entries"][0]["public_id"].as_str().unwrap();
614
    let guess = send(&app, "DELETE", &format!("/runs/{public}"), &[], "").await;
615
    assert_eq!(guess.status(), StatusCode::NOT_FOUND);
616
}
617
618
#[tokio::test]
619
async fn seeded_runs_are_unranked() {
620
    let app = app();
621
    send(&app, "POST", "/runs?seed=7", &[], "").await;
622
    let board = json(send(&app, "GET", "/leaderboard", &[], "").await).await;
623
    assert_eq!(board["entries"].as_array().unwrap().len(), 0);
624
    let all = json(send(&app, "GET", "/leaderboard?seeded=include", &[], "").await).await;
625
    assert_eq!(all["entries"][0]["ranked"], false);
626
}
627
628
#[tokio::test]
629
async fn idle_runs_are_evicted_and_the_server_has_a_limit() {
630
    let forgetful = crate::app(Config {
631
        idle_ttl: Duration::ZERO,
632
        ..Config::default()
633
    });
634
    let first = send(&forgetful, "POST", "/runs", &[], "").await;
635
    let location = header(&first, "location").to_owned();
636
    send(&forgetful, "POST", "/runs", &[], "").await;
637
    let gone = send(&forgetful, "GET", &location, &[], "").await;
638
    assert_eq!(gone.status(), StatusCode::NOT_FOUND);
639
640
    let tiny = crate::app(Config {
641
        max_runs: 1,
642
        ..Config::default()
643
    });
644
    send(&tiny, "POST", "/runs", &[], "").await;
645
    let full = send(&tiny, "POST", "/runs", &[], "").await;
646
    assert_eq!(full.status(), StatusCode::SERVICE_UNAVAILABLE);
647
}
648
649
#[tokio::test]
650
async fn shame_from_two_runs_adds_up() {
651
    let state = Arc::new(AppState::default());
652
    state.insert(fixture());
653
    state.insert(Run::new("deadbeef".into(), "dead".into(), 7));
654
    let app = router(state);
655
    let first = send(&app, "TRACE", "/runs/cafebabe", &[], "").await;
656
    assert_eq!(header(&first, "x-shame"), "1");
657
    let cookie = header(&first, "set-cookie")
658
        .split(';')
659
        .next()
660
        .unwrap()
661
        .to_owned();
662
    let second = send(&app, "TRACE", "/runs/deadbeef", &[("cookie", &cookie)], "").await;
663
    assert_eq!(header(&second, "x-shame"), "2");
664
}
665
666
#[tokio::test]
667
async fn the_root_negotiates() {
668
    let app = app();
669
    let browser = send(&app, "GET", "/", &[("accept", "text/html,*/*")], "").await;
670
    assert_eq!(browser.status(), StatusCode::SEE_OTHER);
671
    assert_eq!(header(&browser, "location"), "/ui/");
672
    let curl = send(&app, "GET", "/", &[("accept", "*/*")], "").await;
673
    assert_eq!(json(curl).await["ui"], "/ui/");
674
    let page = send(&app, "GET", "/ui/", &[], "").await;
675
    assert_eq!(page.status(), StatusCode::OK);
676
}
677
678
#[tokio::test]
679
async fn the_ui_replays_what_browsers_cannot_send() {
680
    let app = app();
681
    let envelope =
682
        r#"{"method": "TRACE", "path": "/graveyard", "headers": [["X-Confession", "I did it"]]}"#;
683
    let traced = json(send(&app, "POST", "/ui/send", &[], envelope).await).await;
684
    assert_eq!(traced["status"], 405);
685
    let envelope = r#"{"method": "GET", "path": "/", "headers": [["Accept", "text/html"]]}"#;
686
    let redirected = json(send(&app, "POST", "/ui/send", &[], envelope).await).await;
687
    assert_eq!(redirected["status"], 303);
688
    assert_eq!(redirected["reason"], "See Other");
689
}
690
691
#[tokio::test]
692
async fn the_ui_decodes_gzip_for_display() {
693
    let app = seeded_with(Floor::Bedroom, |run| run.items.ring = true);
694
    let envelope =
695
        r#"{"method": "GET", "path": "/runs/cafebabe", "headers": [["Accept-Encoding", "gzip"]]}"#;
696
    let shown = json(send(&app, "POST", "/ui/send", &[], envelope).await).await;
697
    assert!(
698
        shown["body"]
699
            .as_str()
700
            .unwrap()
701
            .contains("\"public_id\":\"cafe\"")
702
    );
703
    let encoding = shown["headers"]
704
        .as_array()
705
        .unwrap()
706
        .iter()
707
        .any(|h| h[0] == "content-encoding" && h[1] == "gzip");
708
    assert!(encoding);
709
}
710
711
#[tokio::test]
712
async fn http_1_0_gets_the_pdf_treatment() {
713
    let request = Request::builder()
714
        .uri("/graveyard")
715
        .version(Version::HTTP_10)
716
        .body(Body::empty())
717
        .unwrap();
718
    let response = app().oneshot(request).await.unwrap();
719
    assert_eq!(response.status(), StatusCode::HTTP_VERSION_NOT_SUPPORTED);
720
}
721
722
#[tokio::test]
723
async fn mom_speaks_american() {
724
    let app = seeded_with(Floor::Nans, mum_at_the_door);
725
    let us = [("accept-language", "en-US")];
726
    let knock = send(&app, "GET", "/runs/cafebabe", &us, "").await;
727
    assert_eq!(header(&knock, "content-language"), "en-US");
728
    assert_eq!(header(&knock, "via"), "1.1 mom");
729
    assert!(
730
        json(knock).await["detail"]
731
            .as_str()
732
            .unwrap()
733
            .contains("Someone is at the door.")
734
    );
735
    let excuse = [
736
        ("accept-language", "en-US"),
737
        (
738
            "authorization",
739
            "Digest username=\"carving the turkey\", nonce=\"abc\"",
740
        ),
741
    ];
742
    let answered = send(&app, "GET", "/runs/cafebabe", &excuse, "").await;
743
    assert_eq!(answered.status(), StatusCode::OK);
744
    let floor = json(send(&app, "GET", "/runs/cafebabe/floor", &us, "").await).await;
745
    assert_eq!(floor["name"], "Grandma's house for Thanksgiving");
746
}
747
748
#[tokio::test]
749
async fn the_default_locale_is_british() {
750
    let (app, _) = seeded(Floor::Nans);
751
    let floor = send(&app, "GET", "/runs/cafebabe/floor", &[], "").await;
752
    assert_eq!(header(&floor, "content-language"), "en-GB");
753
    assert!(header(&floor, "vary").contains("Accept-Language"));
754
    assert_eq!(json(floor).await["name"], "Nan's house at Christmas");
755
}
756
757
fn advertised(response: &Response) -> Vec<String> {
758
    header(response, "link")
759
        .split(", <")
760
        .filter(|entry| entry.contains("rels/technique"))
761
        .filter_map(|entry| {
762
            entry
763
                .split("title=\"")
764
                .nth(1)?
765
                .split('"')
766
                .next()
767
                .map(str::to_owned)
768
        })
769
        .collect()
770
}
771
772
#[tokio::test]
773
async fn links_offer_only_usable_techniques() {
774
    let app = seeded_with(Floor::Bedroom, |run| run.foreskin = Some(1.0));
775
    let cold = send(&app, "GET", "/runs/cafebabe", &[], "").await;
776
    assert_eq!(advertised(&cold), ["warmup", "ice"]);
777
    let warm = send(&app, "JELQ", "/runs/cafebabe?technique=warmup", &[], "").await;
778
    let usable = advertised(&warm);
779
    assert!(usable.contains(&"wet".to_owned()) && usable.contains(&"dry".to_owned()));
780
    assert!(!usable.contains(&"uli".to_owned()), "uli needs the forum");
781
    assert!(!usable.contains(&"pump".to_owned()), "pump needs a pump");
782
    assert!(
783
        header(&warm, "link").contains("title=\"wet\"; allow=\"JELQ\"; lubricant=\"required\"")
784
    );
785
786
    let (forum, _) = seeded(Floor::Forum);
787
    let response = send(&forum, "GET", "/runs/cafebabe", &[], "").await;
788
    assert!(advertised(&response).is_empty());
789
}
790
791
fn linked_rels(response: &Response) -> Vec<String> {
792
    header(response, "link")
793
        .split(", <")
794
        .filter_map(|entry| {
795
            entry
796
                .split("rels/")
797
                .nth(1)?
798
                .split('"')
799
                .next()
800
                .map(str::to_owned)
801
        })
802
        .filter(|rel| rel != "technique")
803
        .collect()
804
}
805
806
#[tokio::test]
807
async fn links_hide_resources_that_would_do_nothing() {
808
    let (bedroom, _) = seeded(Floor::Bedroom);
809
    let before = linked_rels(&send(&bedroom, "GET", "/runs/cafebabe", &[], "").await);
810
    assert!(before.contains(&"drawer".to_owned()));
811
    assert!(!before.contains(&"diagnosis".to_owned()));
812
    assert!(!before.contains(&"thread".to_owned()));
813
    let searched = send(&bedroom, "POST", "/runs/cafebabe/drawer", &[], "").await;
814
    assert!(!linked_rels(&searched).contains(&"drawer".to_owned()));
815
816
    let (clinic, _) = seeded(Floor::Clinic);
817
    let rels = linked_rels(&send(&clinic, "GET", "/runs/cafebabe", &[], "").await);
818
    assert!(rels.contains(&"diagnosis".to_owned()) && rels.contains(&"thread".to_owned()));
819
}
820
821
#[tokio::test]
822
async fn ignoring_mum_has_consequences() {
823
    let app = seeded_with(Floor::Bedroom, |run| {
824
        mum_at_the_door(run);
825
        run.here.turns = 6;
826
    });
827
    let knock = send(&app, "GET", "/runs/cafebabe", &[], "").await;
828
    assert_eq!(
829
        json(knock).await["type"],
830
        "https://jelq.dev/problems/someone-at-the-door"
831
    );
832
    let leave = send(&app, "POST", "/runs/cafebabe/floor", &[], "").await;
833
    assert_eq!(
834
        leave.status(),
835
        StatusCode::UNAUTHORIZED,
836
        "no leaving with Mum at the door"
837
    );
838
    send(&app, "GET", "/runs/cafebabe", &[], "").await;
839
    let opened = send(&app, "GET", "/runs/cafebabe", &[], "").await;
840
    assert_eq!(opened.status(), StatusCode::GONE);
841
    assert_eq!(
842
        json(opened).await["type"],
843
        "https://jelq.dev/problems/opened"
844
    );
845
}
846
847
#[tokio::test]
848
async fn caged_drawers_are_noisy_and_eventually_give_up_the_key() {
849
    let app = seeded_with(Floor::ChangingRoom, |run| run.items.cage = true);
850
    let mut statuses = Vec::new();
851
    for _ in 0..12 {
852
        let response = send(&app, "POST", "/runs/cafebabe/drawer", &[], "").await;
853
        statuses.push(response.status());
854
        if response.status() == StatusCode::OK {
855
            break;
856
        }
857
    }
858
    assert_eq!(statuses.last(), Some(&StatusCode::OK), "{statuses:?}");
859
    assert!(
860
        statuses[..statuses.len() - 1]
861
            .iter()
862
            .all(|s| *s == StatusCode::NO_CONTENT)
863
    );
864
}
865
866
#[tokio::test]
867
async fn the_ruler_replaces_the_self_report() {
868
    let app = seeded_with(Floor::Bedroom, |_| {});
869
    let before = send(&app, "GET", "/runs/cafebabe", &[], "").await;
870
    assert!(before.headers().contains_key("x-self-reported-length"));
871
    assert!(!before.headers().contains_key("x-length"));
872
873
    let app = seeded_with(Floor::Bedroom, |run| run.items.ruler = true);
874
    let after = send(&app, "GET", "/runs/cafebabe", &[], "").await;
875
    assert!(!after.headers().contains_key("x-self-reported-length"));
876
    assert!(after.headers().contains_key("x-length"));
877
}
878
879
#[tokio::test]
880
async fn an_open_tab_is_linked_and_jelq_still_answers_with_choices() {
881
    let app = seeded_with(Floor::Bathroom, |run| {
882
        run.story = Story::Pending;
883
        run.warmed_up = true;
884
    });
885
    let response = send(&app, "GET", "/runs/cafebabe", &[], "").await;
886
    let links = header(&response, "link");
887
    assert!(links.contains("/branches/forum>; rel=\"https://jelq.dev/rels/branch\""));
888
    assert!(links.contains("/branches/close-tab>; rel=\"https://jelq.dev/rels/branch\""));
889
    assert!(links.contains("?technique=wet>"));
890
    let jelq = send(&app, "JELQ", "/runs/cafebabe?technique=edge", &[], "").await;
891
    assert_eq!(jelq.status(), StatusCode::MULTIPLE_CHOICES);
892
}
893
894
#[tokio::test]
895
async fn either_story_choice_is_reachable_during_cooldown() {
896
    for choice in ["forum", "close-tab"] {
897
        let app = seeded_with(Floor::Bathroom, |run| {
898
            run.turn = 9;
899
            run.warmed_up = true;
900
            run.stamina = 1;
901
            run.items.door_locks = 1;
902
        });
903
        let tenth = send(&app, "JELQ", "/runs/cafebabe?technique=edge", &[], "").await;
904
        assert_eq!(tenth.status(), StatusCode::OK);
905
        let response = send(&app, "GET", "/runs/cafebabe", &[], "").await;
906
        assert!(!header(&response, "link").contains("?technique="));
907
        let path = format!("/runs/cafebabe/branches/{choice}");
908
        assert!(
909
            header(&response, "link")
910
                .contains(&format!("<{path}>; rel=\"https://jelq.dev/rels/branch\""))
911
        );
912
        let chosen = send(&app, "POST", &path, &[], "").await;
913
        assert_eq!(chosen.status(), StatusCode::SEE_OTHER);
914
        assert!(!header(&chosen, "link").contains("/branches/"));
915
        let repeated = send(&app, "POST", &path, &[], "").await;
916
        assert_eq!(repeated.status(), StatusCode::CONFLICT);
917
    }
918
}
919
920
#[tokio::test]
921
async fn a_door_lock_says_when_it_was_used() {
922
    let app = seeded_with(Floor::Bathroom, |run| {
923
        run.items.door_locks = 1;
924
        run.suspicion = SUSPICION_LIMIT;
925
        run.warmed_up = true;
926
    });
927
    let response = send(&app, "JELQ", "/runs/cafebabe?technique=dry", &[], "").await;
928
    assert_ne!(response.status(), StatusCode::CONFLICT);
929
    assert!(
930
        response
931
            .headers()
932
            .get_all("warning")
933
            .iter()
934
            .any(|w| w.to_str().unwrap().contains("your brother tried the door"))
935
    );
936
}
937
938
#[tokio::test]
939
async fn shopping_survives_browsing_but_not_jelqing() {
940
    let app = seeded_with(Floor::ChangingRoom, |run| run.cash = 50);
941
    send(&app, "GET", "/runs/cafebabe/shop", &[], "").await;
942
    let view = send(&app, "GET", "/runs/cafebabe", &[], "").await;
943
    assert!(header(&view, "link").contains("shop-item"));
944
    let jelq = send(&app, "JELQ", "/runs/cafebabe?technique=warmup", &[], "").await;
945
    assert_ne!(jelq.status(), StatusCode::METHOD_NOT_ALLOWED);
946
    let after = send(&app, "GET", "/runs/cafebabe", &[], "").await;
947
    assert!(!header(&after, "link").contains("shop-item"));
948
}