atagen

jelq

public

an http based roguelike rpg about jelqing

Star0
Watch0
ad00e0e051a1e0a7a31f9089046ffa5ced2726bc
1
ssh://brewery@pls.lobotomise.me:33/atagen/jelq.git
src/tests.rsRust780 lines24.6 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},
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
    });
87
}
88
89
#[tokio::test]
90
async fn creating_a_run_returns_location_and_etag() {
91
    let response = send(&app(), "POST", "/runs?seed=1", &[], "").await;
92
    assert_eq!(response.status(), StatusCode::CREATED);
93
    assert!(header(&response, "location").starts_with("/runs/"));
94
    assert!(response.headers().contains_key(header::ETAG));
95
    assert_eq!(header(&response, "server"), "jelqd/0.6.9");
96
}
97
98
#[tokio::test]
99
async fn bodies_do_not_repeat_the_headers() {
100
    let (app, _) = seeded(Floor::Bedroom);
101
    let response = send(&app, "GET", "/runs/cafebabe", &[], "").await;
102
    assert!(response.headers().contains_key("x-eq"));
103
    let body = json(response).await;
104
    for stat in [
105
        "eq",
106
        "girth",
107
        "foreskin",
108
        "stamina",
109
        "cash",
110
        "self_reported_length",
111
    ] {
112
        assert!(body.get(stat).is_none(), "{stat} is already a header");
113
    }
114
}
115
116
#[tokio::test]
117
async fn jelqing_cold_fails_the_precondition() {
118
    let (app, _) = seeded(Floor::Bedroom);
119
    let response = send(&app, "JELQ", "/runs/cafebabe?technique=stretch", &[], "").await;
120
    assert_eq!(response.status(), StatusCode::PRECONDITION_FAILED);
121
    assert_eq!(
122
        header(&response, "content-type"),
123
        "application/problem+json"
124
    );
125
}
126
127
#[tokio::test]
128
async fn wet_without_lube_is_unsupported_media() {
129
    let (app, _) = seeded(Floor::Bedroom);
130
    send(&app, "JELQ", "/runs/cafebabe?technique=warmup", &[], "").await;
131
    let response = send(&app, "JELQ", "/runs/cafebabe?technique=wet", &[], "").await;
132
    assert_eq!(response.status(), StatusCode::UNSUPPORTED_MEDIA_TYPE);
133
    let images = send(
134
        &app,
135
        "JELQ",
136
        "/runs/cafebabe?technique=wet",
137
        &[("content-type", "image/jpeg")],
138
        "",
139
    )
140
    .await;
141
    assert_eq!(json(images).await["detail"], "No.");
142
}
143
144
#[tokio::test]
145
async fn unknown_methods_are_not_implemented() {
146
    let (app, _) = seeded(Floor::Bedroom);
147
    let response = send(&app, "MILK", "/runs/cafebabe", &[], "").await;
148
    assert_eq!(response.status(), StatusCode::NOT_IMPLEMENTED);
149
    let teapot = send(&app, "BREW", "/runs/cafebabe", &[], "").await;
150
    assert_eq!(teapot.status(), StatusCode::IM_A_TEAPOT);
151
}
152
153
#[tokio::test]
154
async fn the_clinic_does_not_advertise_jelq() {
155
    let (app, _) = seeded(Floor::Clinic);
156
    let options = send(&app, "OPTIONS", "/runs/cafebabe", &[], "").await;
157
    assert!(!header(&options, "allow").contains("JELQ"));
158
    let jelq = send(&app, "JELQ", "/runs/cafebabe?technique=wet", &[], "").await;
159
    assert_eq!(jelq.status(), StatusCode::METHOD_NOT_ALLOWED);
160
    assert!(!header(&jelq, "allow").contains("JELQ"));
161
    let (bedroom, _) = seeded(Floor::Bedroom);
162
    let options = send(&bedroom, "OPTIONS", "/runs/cafebabe", &[], "").await;
163
    assert!(header(&options, "allow").contains("JELQ"));
164
}
165
166
#[tokio::test]
167
async fn patch_only_accepts_the_truth() {
168
    let (app, _) = seeded(Floor::Bedroom);
169
    let patch = [("content-type", "application/json-patch+json")];
170
    let replace = send(
171
        &app,
172
        "PATCH",
173
        "/runs/cafebabe",
174
        &patch,
175
        r#"[{"op":"replace","path":"/length","value":12}]"#,
176
    )
177
    .await;
178
    assert_eq!(replace.status(), StatusCode::UNPROCESSABLE_ENTITY);
179
    let lie = send(
180
        &app,
181
        "PATCH",
182
        "/runs/cafebabe",
183
        &patch,
184
        r#"[{"op":"test","path":"/length","value":40}]"#,
185
    )
186
    .await;
187
    assert_eq!(
188
        json(lie).await["type"],
189
        "https://jelq.dev/problems/exaggeration"
190
    );
191
}
192
193
#[tokio::test]
194
async fn deleting_the_foreskin_requires_certainty() {
195
    let (app, _) = seeded(Floor::Bedroom);
196
    let response = send(&app, "DELETE", "/runs/cafebabe/foreskin", &[], "").await;
197
    assert_eq!(response.status(), StatusCode::PRECONDITION_REQUIRED);
198
    let look = send(&app, "GET", "/runs/cafebabe/foreskin", &[], "").await;
199
    assert_eq!(look.status(), StatusCode::NO_CONTENT);
200
    let etag = header(&look, "etag").to_owned();
201
    let response = send(
202
        &app,
203
        "DELETE",
204
        "/runs/cafebabe/foreskin",
205
        &[("if-match", &etag)],
206
        "",
207
    )
208
    .await;
209
    assert_eq!(response.status(), StatusCode::NO_CONTENT);
210
    let response = send(&app, "GET", "/runs/cafebabe/foreskin", &[], "").await;
211
    assert_eq!(response.status(), StatusCode::GONE);
212
}
213
214
#[tokio::test]
215
async fn length_is_chunked_until_the_ruler() {
216
    let (app, _) = seeded(Floor::Bedroom);
217
    let lie = send(&app, "GET", "/runs/cafebabe/length", &[], "").await;
218
    assert!(!lie.headers().contains_key(header::CONTENT_LENGTH));
219
    let app = seeded_with(Floor::Bedroom, |run| run.items.ruler = true);
220
    let truth = send(&app, "GET", "/runs/cafebabe/length", &[], "").await;
221
    let declared: usize = header(&truth, "content-length").parse().unwrap();
222
    let body = text(truth).await;
223
    assert_eq!(body.len(), declared);
224
    assert!(body.starts_with("8=") && body.ends_with('D'));
225
}
226
227
#[tokio::test]
228
async fn length_supports_inch_ranges() {
229
    let (app, _) = seeded(Floor::Bedroom);
230
    let response = send(
231
        &app,
232
        "GET",
233
        "/runs/cafebabe/length",
234
        &[("range", "inches=0-1")],
235
        "",
236
    )
237
    .await;
238
    assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT);
239
    assert!(header(&response, "content-range").starts_with("inches 0.00-1.00/"));
240
    assert_eq!(text(response).await.len(), 25);
241
    let absurd = send(
242
        &app,
243
        "GET",
244
        "/runs/cafebabe/length",
245
        &[("range", "inches=40-50")],
246
        "",
247
    )
248
    .await;
249
    assert_eq!(absurd.status(), StatusCode::RANGE_NOT_SATISFIABLE);
250
}
251
252
#[tokio::test]
253
async fn the_clinic_escalates_toward_honesty() {
254
    let (app, _) = seeded(Floor::Clinic);
255
    let mut hints = Vec::new();
256
    for _ in 0..3 {
257
        let response = send(
258
            &app,
259
            "GET",
260
            "/runs/cafebabe/diagnosis",
261
            &[("authorization", "Excuse \"changing\"")],
262
            "",
263
        )
264
        .await;
265
        assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
266
        hints.push(header(&response, "www-authenticate").to_owned());
267
    }
268
    assert!(hints[2].contains("Be honest."));
269
}
270
271
#[tokio::test]
272
async fn the_true_ending_is_not_modified() {
273
    let (early, _) = seeded(Floor::Bedroom);
274
    let response = send(&early, "GET", "/runs/cafebabe/diagnosis", &[], "").await;
275
    assert_eq!(response.status(), StatusCode::TOO_EARLY);
276
277
    let (app, etag) = seeded(Floor::Clinic);
278
    assert!(
279
        !etag.contains("cafebabe"),
280
        "the origin ETag must not be derivable from the id"
281
    );
282
    let response = send(
283
        &app,
284
        "GET",
285
        "/runs/cafebabe/diagnosis",
286
        &[
287
            ("authorization", "Bearer honesty"),
288
            ("if-none-match", &etag),
289
        ],
290
        "",
291
    )
292
    .await;
293
    assert_eq!(response.status(), StatusCode::NOT_MODIFIED);
294
    assert_eq!(header(&response, "x-diagnosis"), "normal");
295
}
296
297
#[tokio::test]
298
async fn mum_accepts_a_known_excuse() {
299
    let app = seeded_with(Floor::Bedroom, mum_at_the_door);
300
    let response = send(&app, "GET", "/runs/cafebabe", &[], "").await;
301
    assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
302
    assert_eq!(header(&response, "via"), "1.1 mum");
303
    let response = send(
304
        &app,
305
        "GET",
306
        "/runs/cafebabe",
307
        &[("authorization", "Excuse \"homework\"")],
308
        "",
309
    )
310
    .await;
311
    assert_eq!(response.status(), StatusCode::OK);
312
}
313
314
#[tokio::test]
315
async fn mum_runs_out_of_patience() {
316
    let app = seeded_with(Floor::Bedroom, mum_at_the_door);
317
    let lie = [("authorization", "Excuse \"it is for science\"")];
318
    for _ in 0..2 {
319
        let response = send(&app, "GET", "/runs/cafebabe", &lie, "").await;
320
        assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
321
    }
322
    let caught = send(&app, "GET", "/runs/cafebabe", &lie, "").await;
323
    assert_eq!(caught.status(), StatusCode::GONE);
324
    assert_eq!(
325
        json(caught).await["type"],
326
        "https://jelq.dev/problems/caught"
327
    );
328
}
329
330
#[tokio::test]
331
async fn mum_remembers_excuses_by_christmas() {
332
    let app = seeded_with(Floor::Nans, |run| {
333
        mum_at_the_door(run);
334
        run.heard.push("homework".into());
335
    });
336
    let basic = send(
337
        &app,
338
        "GET",
339
        "/runs/cafebabe",
340
        &[("authorization", "Excuse \"changing\"")],
341
        "",
342
    )
343
    .await;
344
    assert_eq!(basic.status(), StatusCode::UNAUTHORIZED);
345
    assert!(header(&basic, "www-authenticate").starts_with("Digest"));
346
    let replay = send(
347
        &app,
348
        "GET",
349
        "/runs/cafebabe",
350
        &[(
351
            "authorization",
352
            "Digest username=\"homework\", nonce=\"abc\"",
353
        )],
354
        "",
355
    )
356
    .await;
357
    assert_eq!(
358
        json(replay).await["type"],
359
        "https://jelq.dev/problems/excuse-replayed"
360
    );
361
    let fresh = send(
362
        &app,
363
        "GET",
364
        "/runs/cafebabe",
365
        &[(
366
            "authorization",
367
            "Digest username=\"wrapping presents\", nonce=\"abc\"",
368
        )],
369
        "",
370
    )
371
    .await;
372
    assert_eq!(fresh.status(), StatusCode::OK);
373
}
374
375
#[tokio::test]
376
async fn a_pulled_muscle_is_a_true_excuse() {
377
    let app = seeded_with(Floor::Nans, |run| {
378
        mum_at_the_door(run);
379
        run.conditions.pulled_muscle = true;
380
        run.heard.push("pulled a muscle".into());
381
    });
382
    let response = send(
383
        &app,
384
        "GET",
385
        "/runs/cafebabe",
386
        &[(
387
            "authorization",
388
            "Digest username=\"pulled a muscle\", nonce=\"abc\"",
389
        )],
390
        "",
391
    )
392
    .await;
393
    assert_eq!(response.status(), StatusCode::OK);
394
    assert!(header(&response, "x-stamina").ends_with("/7"));
395
}
396
397
#[tokio::test]
398
async fn floors_end_when_they_end() {
399
    let app = seeded_with(Floor::Bedroom, |_| {});
400
    let early = send(&app, "POST", "/runs/cafebabe/floor", &[], "").await;
401
    assert_eq!(early.status(), StatusCode::TOO_EARLY);
402
    let app = seeded_with(Floor::Bedroom, |run| run.here.turns = 6);
403
    let next = send(&app, "POST", "/runs/cafebabe/floor", &[], "").await;
404
    assert!(
405
        next.status() == StatusCode::SEE_OTHER || next.status() == StatusCode::CONFLICT,
406
        "either the floor changed or the zip did"
407
    );
408
}
409
410
#[tokio::test]
411
async fn pump_bro_upsells() {
412
    let closed = seeded_with(Floor::Bedroom, |_| {});
413
    let response = send(&closed, "GET", "/runs/cafebabe/shop", &[], "").await;
414
    assert_eq!(response.status(), StatusCode::NOT_FOUND);
415
416
    let app = seeded_with(Floor::ChangingRoom, |_| {});
417
    let pump = send(&app, "POST", "/runs/cafebabe/shop/pump", &[], "").await;
418
    assert_eq!(pump.status(), StatusCode::UPGRADE_REQUIRED);
419
    assert_eq!(header(&pump, "upgrade"), "pump-pro/2.0");
420
    let pro = send(&app, "POST", "/runs/cafebabe/shop/pump-pro", &[], "").await;
421
    assert_eq!(pro.status(), StatusCode::CREATED);
422
    let broke = send(&app, "POST", "/runs/cafebabe/shop/extender", &[], "").await;
423
    assert_eq!(broke.status(), StatusCode::PAYMENT_REQUIRED);
424
}
425
426
#[tokio::test]
427
async fn only_the_key_opens_the_cage() {
428
    let app = seeded_with(Floor::ChangingRoom, |run| run.items.cage = true);
429
    send(&app, "POST", "/runs/cafebabe/shop/ice-pack", &[], "").await;
430
    let still = send(
431
        &app,
432
        "GET",
433
        "/runs/cafebabe/inventory/chastity-cage",
434
        &[],
435
        "",
436
    )
437
    .await;
438
    assert_eq!(still.status(), StatusCode::OK);
439
    send(&app, "POST", "/runs/cafebabe/shop/cage-key", &[], "").await;
440
    let gone = send(
441
        &app,
442
        "GET",
443
        "/runs/cafebabe/inventory/chastity-cage",
444
        &[],
445
        "",
446
    )
447
    .await;
448
    assert_eq!(gone.status(), StatusCode::NOT_FOUND);
449
}
450
451
#[tokio::test]
452
async fn combos_are_multi_status_xml() {
453
    let app = seeded_with(Floor::Bathroom, |run| {
454
        run.foreskin = Some(1.0);
455
        run.items.blinds = true;
456
    });
457
    send(&app, "JELQ", "/runs/cafebabe?technique=warmup", &[], "").await;
458
    let response = send(
459
        &app,
460
        "JELQ",
461
        "/runs/cafebabe?technique=wet,stretch",
462
        &[("content-type", "application/baby-oil")],
463
        "",
464
    )
465
    .await;
466
    assert_eq!(response.status(), StatusCode::MULTI_STATUS);
467
    assert!(text(response).await.contains("<D:multistatus"));
468
}
469
470
#[tokio::test]
471
async fn the_elder_is_stale() {
472
    let app = seeded_with(Floor::Forum, |run| run.length = 5.0);
473
    let unverified = send(&app, "GET", "/runs/cafebabe/elder", &[], "").await;
474
    assert_eq!(
475
        unverified.status(),
476
        StatusCode::PROXY_AUTHENTICATION_REQUIRED
477
    );
478
    let lie = send(
479
        &app,
480
        "GET",
481
        "/runs/cafebabe/elder",
482
        &[("proxy-authorization", "Ruler 9")],
483
        "",
484
    )
485
    .await;
486
    assert_eq!(lie.status(), StatusCode::BAD_REQUEST);
487
    let csv = send(
488
        &app,
489
        "GET",
490
        "/runs/cafebabe/elder",
491
        &[("proxy-authorization", "Ruler 5.0")],
492
        "",
493
    )
494
    .await;
495
    assert_eq!(csv.status(), StatusCode::OK);
496
    let stale = send(
497
        &app,
498
        "GET",
499
        "/runs/cafebabe/elder",
500
        &[("if-modified-since", "Mon, 15 Jun 2009 00:00:00 GMT")],
501
        "",
502
    )
503
    .await;
504
    assert_eq!(stale.status(), StatusCode::NOT_MODIFIED);
505
}
506
507
#[tokio::test]
508
async fn the_displayed_length_is_accepted_as_honest() {
509
    let app = seeded_with(Floor::Forum, |run| run.length = 5.2496);
510
    let response = send(
511
        &app,
512
        "GET",
513
        "/runs/cafebabe/elder",
514
        &[("proxy-authorization", "Ruler 5.25")],
515
        "",
516
    )
517
    .await;
518
    assert_eq!(response.status(), StatusCode::OK);
519
}
520
521
#[tokio::test]
522
async fn the_hotel_has_a_captive_portal() {
523
    let app = seeded_with(Floor::Hotel, |_| {});
524
    let blocked = send(&app, "GET", "/runs/cafebabe", &[], "").await;
525
    assert_eq!(
526
        blocked.status(),
527
        StatusCode::NETWORK_AUTHENTICATION_REQUIRED
528
    );
529
    let accepted = send(&app, "POST", "/runs/cafebabe/portal", &[], "").await;
530
    assert_eq!(accepted.status(), StatusCode::SEE_OTHER);
531
    let online = send(&app, "GET", "/runs/cafebabe", &[], "").await;
532
    assert_eq!(online.status(), StatusCode::OK);
533
}
534
535
#[tokio::test]
536
async fn the_internet_never_forgets() {
537
    let app = seeded_with(Floor::Bedroom, |run| {
538
        run.filmed_at = Some(std::time::SystemTime::now());
539
    });
540
    let footage = send(&app, "GET", "/runs/cafebabe/footage", &[], "").await;
541
    assert_eq!(footage.status(), StatusCode::OK);
542
    assert_eq!(
543
        header(&footage, "cache-control"),
544
        "public, max-age=31536000, immutable"
545
    );
546
    assert!(footage.headers().contains_key(header::AGE));
547
    let delete = send(&app, "DELETE", "/runs/cafebabe/footage", &[], "").await;
548
    assert_eq!(delete.status(), StatusCode::METHOD_NOT_ALLOWED);
549
    assert!(!header(&delete, "allow").contains("DELETE"));
550
}
551
552
#[tokio::test]
553
async fn forged_ruler_cookies_are_lies() {
554
    let forged = send(
555
        &app(),
556
        "GET",
557
        "/leaderboard?sort=content-length",
558
        &[("cookie", "ruler=1")],
559
        "",
560
    )
561
    .await;
562
    assert_eq!(forged.status(), StatusCode::BAD_REQUEST);
563
}
564
565
#[tokio::test]
566
async fn listings_never_reveal_the_private_id() {
567
    let app = app();
568
    let created = send(&app, "POST", "/runs", &[], "").await;
569
    let location = header(&created, "location").to_owned();
570
    let board = text(send(&app, "GET", "/leaderboard", &[], "").await).await;
571
    let private = location.trim_start_matches("/runs/");
572
    assert!(!board.contains(private));
573
    let entry: Value = serde_json::from_str(&board).unwrap();
574
    let public = entry["entries"][0]["public_id"].as_str().unwrap();
575
    let guess = send(&app, "DELETE", &format!("/runs/{public}"), &[], "").await;
576
    assert_eq!(guess.status(), StatusCode::NOT_FOUND);
577
}
578
579
#[tokio::test]
580
async fn seeded_runs_are_unranked() {
581
    let app = app();
582
    send(&app, "POST", "/runs?seed=7", &[], "").await;
583
    let board = json(send(&app, "GET", "/leaderboard", &[], "").await).await;
584
    assert_eq!(board["entries"].as_array().unwrap().len(), 0);
585
    let all = json(send(&app, "GET", "/leaderboard?seeded=include", &[], "").await).await;
586
    assert_eq!(all["entries"][0]["ranked"], false);
587
}
588
589
#[tokio::test]
590
async fn idle_runs_are_evicted_and_the_server_has_a_limit() {
591
    let forgetful = crate::app(Config {
592
        idle_ttl: Duration::ZERO,
593
        ..Config::default()
594
    });
595
    let first = send(&forgetful, "POST", "/runs", &[], "").await;
596
    let location = header(&first, "location").to_owned();
597
    send(&forgetful, "POST", "/runs", &[], "").await;
598
    let gone = send(&forgetful, "GET", &location, &[], "").await;
599
    assert_eq!(gone.status(), StatusCode::NOT_FOUND);
600
601
    let tiny = crate::app(Config {
602
        max_runs: 1,
603
        ..Config::default()
604
    });
605
    send(&tiny, "POST", "/runs", &[], "").await;
606
    let full = send(&tiny, "POST", "/runs", &[], "").await;
607
    assert_eq!(full.status(), StatusCode::SERVICE_UNAVAILABLE);
608
}
609
610
#[tokio::test]
611
async fn shame_from_two_runs_adds_up() {
612
    let state = Arc::new(AppState::default());
613
    state.insert(fixture());
614
    state.insert(Run::new("deadbeef".into(), "dead".into(), 7));
615
    let app = router(state);
616
    let first = send(&app, "TRACE", "/runs/cafebabe", &[], "").await;
617
    assert_eq!(header(&first, "x-shame"), "1");
618
    let cookie = header(&first, "set-cookie")
619
        .split(';')
620
        .next()
621
        .unwrap()
622
        .to_owned();
623
    let second = send(&app, "TRACE", "/runs/deadbeef", &[("cookie", &cookie)], "").await;
624
    assert_eq!(header(&second, "x-shame"), "2");
625
}
626
627
#[tokio::test]
628
async fn the_root_negotiates() {
629
    let app = app();
630
    let browser = send(&app, "GET", "/", &[("accept", "text/html,*/*")], "").await;
631
    assert_eq!(browser.status(), StatusCode::SEE_OTHER);
632
    assert_eq!(header(&browser, "location"), "/ui/");
633
    let curl = send(&app, "GET", "/", &[("accept", "*/*")], "").await;
634
    assert_eq!(json(curl).await["ui"], "/ui/");
635
    let page = send(&app, "GET", "/ui/", &[], "").await;
636
    assert_eq!(page.status(), StatusCode::OK);
637
}
638
639
#[tokio::test]
640
async fn the_ui_replays_what_browsers_cannot_send() {
641
    let app = app();
642
    let envelope =
643
        r#"{"method": "TRACE", "path": "/graveyard", "headers": [["X-Confession", "I did it"]]}"#;
644
    let traced = json(send(&app, "POST", "/ui/send", &[], envelope).await).await;
645
    assert_eq!(traced["status"], 405);
646
    let envelope = r#"{"method": "GET", "path": "/", "headers": [["Accept", "text/html"]]}"#;
647
    let redirected = json(send(&app, "POST", "/ui/send", &[], envelope).await).await;
648
    assert_eq!(redirected["status"], 303);
649
    assert_eq!(redirected["reason"], "See Other");
650
}
651
652
#[tokio::test]
653
async fn the_ui_decodes_gzip_for_display() {
654
    let app = seeded_with(Floor::Bedroom, |run| run.items.ring = true);
655
    let envelope =
656
        r#"{"method": "GET", "path": "/runs/cafebabe", "headers": [["Accept-Encoding", "gzip"]]}"#;
657
    let shown = json(send(&app, "POST", "/ui/send", &[], envelope).await).await;
658
    assert!(
659
        shown["body"]
660
            .as_str()
661
            .unwrap()
662
            .contains("\"public_id\":\"cafe\"")
663
    );
664
    let encoding = shown["headers"]
665
        .as_array()
666
        .unwrap()
667
        .iter()
668
        .any(|h| h[0] == "content-encoding" && h[1] == "gzip");
669
    assert!(encoding);
670
}
671
672
#[tokio::test]
673
async fn http_1_0_gets_the_pdf_treatment() {
674
    let request = Request::builder()
675
        .uri("/graveyard")
676
        .version(Version::HTTP_10)
677
        .body(Body::empty())
678
        .unwrap();
679
    let response = app().oneshot(request).await.unwrap();
680
    assert_eq!(response.status(), StatusCode::HTTP_VERSION_NOT_SUPPORTED);
681
}
682
683
#[tokio::test]
684
async fn mom_speaks_american() {
685
    let app = seeded_with(Floor::Nans, mum_at_the_door);
686
    let us = [("accept-language", "en-US")];
687
    let knock = send(&app, "GET", "/runs/cafebabe", &us, "").await;
688
    assert_eq!(header(&knock, "content-language"), "en-US");
689
    assert_eq!(header(&knock, "via"), "1.1 mom");
690
    assert!(
691
        json(knock).await["detail"]
692
            .as_str()
693
            .unwrap()
694
            .contains("Someone is at the door.")
695
    );
696
    let excuse = [
697
        ("accept-language", "en-US"),
698
        (
699
            "authorization",
700
            "Digest username=\"carving the turkey\", nonce=\"abc\"",
701
        ),
702
    ];
703
    let answered = send(&app, "GET", "/runs/cafebabe", &excuse, "").await;
704
    assert_eq!(answered.status(), StatusCode::OK);
705
    let floor = json(send(&app, "GET", "/runs/cafebabe/floor", &us, "").await).await;
706
    assert_eq!(floor["name"], "Grandma's house for Thanksgiving");
707
}
708
709
#[tokio::test]
710
async fn the_default_locale_is_british() {
711
    let (app, _) = seeded(Floor::Nans);
712
    let floor = send(&app, "GET", "/runs/cafebabe/floor", &[], "").await;
713
    assert_eq!(header(&floor, "content-language"), "en-GB");
714
    assert!(header(&floor, "vary").contains("Accept-Language"));
715
    assert_eq!(json(floor).await["name"], "Nan's house at Christmas");
716
}
717
718
fn advertised(response: &Response) -> Vec<String> {
719
    header(response, "link")
720
        .split(", <")
721
        .filter(|entry| entry.contains("rels/technique"))
722
        .filter_map(|entry| {
723
            entry
724
                .split("title=\"")
725
                .nth(1)?
726
                .split('"')
727
                .next()
728
                .map(str::to_owned)
729
        })
730
        .collect()
731
}
732
733
#[tokio::test]
734
async fn links_offer_only_usable_techniques() {
735
    let app = seeded_with(Floor::Bedroom, |run| run.foreskin = Some(1.0));
736
    let cold = send(&app, "GET", "/runs/cafebabe", &[], "").await;
737
    assert_eq!(advertised(&cold), ["warmup", "ice"]);
738
    let warm = send(&app, "JELQ", "/runs/cafebabe?technique=warmup", &[], "").await;
739
    let usable = advertised(&warm);
740
    assert!(usable.contains(&"wet".to_owned()) && usable.contains(&"dry".to_owned()));
741
    assert!(!usable.contains(&"uli".to_owned()), "uli needs the forum");
742
    assert!(!usable.contains(&"pump".to_owned()), "pump needs a pump");
743
    assert!(
744
        header(&warm, "link").contains("title=\"wet\"; allow=\"JELQ\"; lubricant=\"required\"")
745
    );
746
747
    let (forum, _) = seeded(Floor::Forum);
748
    let response = send(&forum, "GET", "/runs/cafebabe", &[], "").await;
749
    assert!(advertised(&response).is_empty());
750
}
751
752
fn linked_rels(response: &Response) -> Vec<String> {
753
    header(response, "link")
754
        .split(", <")
755
        .filter_map(|entry| {
756
            entry
757
                .split("rels/")
758
                .nth(1)?
759
                .split('"')
760
                .next()
761
                .map(str::to_owned)
762
        })
763
        .filter(|rel| rel != "technique")
764
        .collect()
765
}
766
767
#[tokio::test]
768
async fn links_hide_resources_that_would_do_nothing() {
769
    let (bedroom, _) = seeded(Floor::Bedroom);
770
    let before = linked_rels(&send(&bedroom, "GET", "/runs/cafebabe", &[], "").await);
771
    assert!(before.contains(&"drawer".to_owned()));
772
    assert!(!before.contains(&"diagnosis".to_owned()));
773
    assert!(!before.contains(&"thread".to_owned()));
774
    let searched = send(&bedroom, "POST", "/runs/cafebabe/drawer", &[], "").await;
775
    assert!(!linked_rels(&searched).contains(&"drawer".to_owned()));
776
777
    let (clinic, _) = seeded(Floor::Clinic);
778
    let rels = linked_rels(&send(&clinic, "GET", "/runs/cafebabe", &[], "").await);
779
    assert!(rels.contains(&"diagnosis".to_owned()) && rels.contains(&"thread".to_owned()));
780
}