atagen

jelq

public

an http based roguelike rpg about jelqing

Star0
Watch0
9683736a2c5e4af0c861af0df5b33ec31e26cefa
1
ssh://brewery@pls.lobotomise.me:33/atagen/jelq.git
src/tests.rsRust682 lines21.1 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 found = send(&app, "POST", "/runs/cafebabe/drawer", &[], "").await;
220
    assert_eq!(found.status(), StatusCode::FOUND);
221
    let truth = send(&app, "GET", "/runs/cafebabe/length", &[], "").await;
222
    let declared: usize = header(&truth, "content-length").parse().unwrap();
223
    let body = text(truth).await;
224
    assert_eq!(body.len(), declared);
225
    assert!(body.starts_with("8=") && body.ends_with('D'));
226
}
227
228
#[tokio::test]
229
async fn length_supports_inch_ranges() {
230
    let (app, _) = seeded(Floor::Bedroom);
231
    let response = send(
232
        &app,
233
        "GET",
234
        "/runs/cafebabe/length",
235
        &[("range", "inches=0-1")],
236
        "",
237
    )
238
    .await;
239
    assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT);
240
    assert!(header(&response, "content-range").starts_with("inches 0.00-1.00/"));
241
    assert_eq!(text(response).await.len(), 25);
242
    let absurd = send(
243
        &app,
244
        "GET",
245
        "/runs/cafebabe/length",
246
        &[("range", "inches=40-50")],
247
        "",
248
    )
249
    .await;
250
    assert_eq!(absurd.status(), StatusCode::RANGE_NOT_SATISFIABLE);
251
}
252
253
#[tokio::test]
254
async fn the_clinic_escalates_toward_honesty() {
255
    let (app, _) = seeded(Floor::Clinic);
256
    let mut hints = Vec::new();
257
    for _ in 0..3 {
258
        let response = send(
259
            &app,
260
            "GET",
261
            "/runs/cafebabe/diagnosis",
262
            &[("authorization", "Excuse \"changing\"")],
263
            "",
264
        )
265
        .await;
266
        assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
267
        hints.push(header(&response, "www-authenticate").to_owned());
268
    }
269
    assert!(hints[2].contains("Be honest."));
270
}
271
272
#[tokio::test]
273
async fn the_true_ending_is_not_modified() {
274
    let (early, _) = seeded(Floor::Bedroom);
275
    let response = send(&early, "GET", "/runs/cafebabe/diagnosis", &[], "").await;
276
    assert_eq!(response.status(), StatusCode::TOO_EARLY);
277
278
    let (app, etag) = seeded(Floor::Clinic);
279
    assert!(
280
        !etag.contains("cafebabe"),
281
        "the origin ETag must not be derivable from the id"
282
    );
283
    let response = send(
284
        &app,
285
        "GET",
286
        "/runs/cafebabe/diagnosis",
287
        &[
288
            ("authorization", "Bearer honesty"),
289
            ("if-none-match", &etag),
290
        ],
291
        "",
292
    )
293
    .await;
294
    assert_eq!(response.status(), StatusCode::NOT_MODIFIED);
295
    assert_eq!(header(&response, "x-diagnosis"), "normal");
296
}
297
298
#[tokio::test]
299
async fn mum_accepts_a_known_excuse() {
300
    let app = seeded_with(Floor::Bedroom, mum_at_the_door);
301
    let response = send(&app, "GET", "/runs/cafebabe", &[], "").await;
302
    assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
303
    assert_eq!(header(&response, "via"), "1.1 mum");
304
    let response = send(
305
        &app,
306
        "GET",
307
        "/runs/cafebabe",
308
        &[("authorization", "Excuse \"homework\"")],
309
        "",
310
    )
311
    .await;
312
    assert_eq!(response.status(), StatusCode::OK);
313
}
314
315
#[tokio::test]
316
async fn mum_runs_out_of_patience() {
317
    let app = seeded_with(Floor::Bedroom, mum_at_the_door);
318
    let lie = [("authorization", "Excuse \"it is for science\"")];
319
    for _ in 0..2 {
320
        let response = send(&app, "GET", "/runs/cafebabe", &lie, "").await;
321
        assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
322
    }
323
    let caught = send(&app, "GET", "/runs/cafebabe", &lie, "").await;
324
    assert_eq!(caught.status(), StatusCode::GONE);
325
    assert_eq!(
326
        json(caught).await["type"],
327
        "https://jelq.dev/problems/caught"
328
    );
329
}
330
331
#[tokio::test]
332
async fn mum_remembers_excuses_by_christmas() {
333
    let app = seeded_with(Floor::Nans, |run| {
334
        mum_at_the_door(run);
335
        run.heard.push("homework".into());
336
    });
337
    let basic = send(
338
        &app,
339
        "GET",
340
        "/runs/cafebabe",
341
        &[("authorization", "Excuse \"changing\"")],
342
        "",
343
    )
344
    .await;
345
    assert_eq!(basic.status(), StatusCode::UNAUTHORIZED);
346
    assert!(header(&basic, "www-authenticate").starts_with("Digest"));
347
    let replay = send(
348
        &app,
349
        "GET",
350
        "/runs/cafebabe",
351
        &[(
352
            "authorization",
353
            "Digest username=\"homework\", nonce=\"abc\"",
354
        )],
355
        "",
356
    )
357
    .await;
358
    assert_eq!(
359
        json(replay).await["type"],
360
        "https://jelq.dev/problems/excuse-replayed"
361
    );
362
    let fresh = send(
363
        &app,
364
        "GET",
365
        "/runs/cafebabe",
366
        &[(
367
            "authorization",
368
            "Digest username=\"wrapping presents\", nonce=\"abc\"",
369
        )],
370
        "",
371
    )
372
    .await;
373
    assert_eq!(fresh.status(), StatusCode::OK);
374
}
375
376
#[tokio::test]
377
async fn a_pulled_muscle_is_a_true_excuse() {
378
    let app = seeded_with(Floor::Nans, |run| {
379
        mum_at_the_door(run);
380
        run.conditions.pulled_muscle = true;
381
        run.heard.push("pulled a muscle".into());
382
    });
383
    let response = send(
384
        &app,
385
        "GET",
386
        "/runs/cafebabe",
387
        &[(
388
            "authorization",
389
            "Digest username=\"pulled a muscle\", nonce=\"abc\"",
390
        )],
391
        "",
392
    )
393
    .await;
394
    assert_eq!(response.status(), StatusCode::OK);
395
    assert!(header(&response, "x-stamina").ends_with("/7"));
396
}
397
398
#[tokio::test]
399
async fn floors_end_when_they_end() {
400
    let app = seeded_with(Floor::Bedroom, |_| {});
401
    let early = send(&app, "POST", "/runs/cafebabe/floor", &[], "").await;
402
    assert_eq!(early.status(), StatusCode::TOO_EARLY);
403
    let app = seeded_with(Floor::Bedroom, |run| run.here.turns = 6);
404
    let next = send(&app, "POST", "/runs/cafebabe/floor", &[], "").await;
405
    assert!(
406
        next.status() == StatusCode::SEE_OTHER || next.status() == StatusCode::CONFLICT,
407
        "either the floor changed or the zip did"
408
    );
409
}
410
411
#[tokio::test]
412
async fn pump_bro_upsells() {
413
    let closed = seeded_with(Floor::Bedroom, |_| {});
414
    let response = send(&closed, "GET", "/runs/cafebabe/shop", &[], "").await;
415
    assert_eq!(response.status(), StatusCode::NOT_FOUND);
416
417
    let app = seeded_with(Floor::ChangingRoom, |_| {});
418
    let pump = send(&app, "POST", "/runs/cafebabe/shop/pump", &[], "").await;
419
    assert_eq!(pump.status(), StatusCode::UPGRADE_REQUIRED);
420
    assert_eq!(header(&pump, "upgrade"), "pump-pro/2.0");
421
    let pro = send(&app, "POST", "/runs/cafebabe/shop/pump-pro", &[], "").await;
422
    assert_eq!(pro.status(), StatusCode::CREATED);
423
    let broke = send(&app, "POST", "/runs/cafebabe/shop/extender", &[], "").await;
424
    assert_eq!(broke.status(), StatusCode::PAYMENT_REQUIRED);
425
}
426
427
#[tokio::test]
428
async fn only_the_key_opens_the_cage() {
429
    let app = seeded_with(Floor::ChangingRoom, |run| run.items.cage = true);
430
    send(&app, "POST", "/runs/cafebabe/shop/ice-pack", &[], "").await;
431
    let still = send(
432
        &app,
433
        "GET",
434
        "/runs/cafebabe/inventory/chastity-cage",
435
        &[],
436
        "",
437
    )
438
    .await;
439
    assert_eq!(still.status(), StatusCode::OK);
440
    send(&app, "POST", "/runs/cafebabe/shop/cage-key", &[], "").await;
441
    let gone = send(
442
        &app,
443
        "GET",
444
        "/runs/cafebabe/inventory/chastity-cage",
445
        &[],
446
        "",
447
    )
448
    .await;
449
    assert_eq!(gone.status(), StatusCode::NOT_FOUND);
450
}
451
452
#[tokio::test]
453
async fn combos_are_multi_status_xml() {
454
    let app = seeded_with(Floor::Bathroom, |run| {
455
        run.foreskin = Some(1.0);
456
        run.items.blinds = true;
457
    });
458
    send(&app, "JELQ", "/runs/cafebabe?technique=warmup", &[], "").await;
459
    let response = send(
460
        &app,
461
        "JELQ",
462
        "/runs/cafebabe?technique=wet,stretch",
463
        &[("content-type", "application/baby-oil")],
464
        "",
465
    )
466
    .await;
467
    assert_eq!(response.status(), StatusCode::MULTI_STATUS);
468
    assert!(text(response).await.contains("<D:multistatus"));
469
}
470
471
#[tokio::test]
472
async fn the_elder_is_stale() {
473
    let app = seeded_with(Floor::Forum, |run| run.length = 5.0);
474
    let unverified = send(&app, "GET", "/runs/cafebabe/elder", &[], "").await;
475
    assert_eq!(
476
        unverified.status(),
477
        StatusCode::PROXY_AUTHENTICATION_REQUIRED
478
    );
479
    let lie = send(
480
        &app,
481
        "GET",
482
        "/runs/cafebabe/elder",
483
        &[("proxy-authorization", "Ruler 9")],
484
        "",
485
    )
486
    .await;
487
    assert_eq!(lie.status(), StatusCode::BAD_REQUEST);
488
    let csv = send(
489
        &app,
490
        "GET",
491
        "/runs/cafebabe/elder",
492
        &[("proxy-authorization", "Ruler 5.0")],
493
        "",
494
    )
495
    .await;
496
    assert_eq!(csv.status(), StatusCode::OK);
497
    let stale = send(
498
        &app,
499
        "GET",
500
        "/runs/cafebabe/elder",
501
        &[("if-modified-since", "Mon, 15 Jun 2009 00:00:00 GMT")],
502
        "",
503
    )
504
    .await;
505
    assert_eq!(stale.status(), StatusCode::NOT_MODIFIED);
506
}
507
508
#[tokio::test]
509
async fn the_displayed_length_is_accepted_as_honest() {
510
    let app = seeded_with(Floor::Forum, |run| run.length = 5.2496);
511
    let response = send(
512
        &app,
513
        "GET",
514
        "/runs/cafebabe/elder",
515
        &[("proxy-authorization", "Ruler 5.25")],
516
        "",
517
    )
518
    .await;
519
    assert_eq!(response.status(), StatusCode::OK);
520
}
521
522
#[tokio::test]
523
async fn the_hotel_has_a_captive_portal() {
524
    let app = seeded_with(Floor::Hotel, |_| {});
525
    let blocked = send(&app, "GET", "/runs/cafebabe", &[], "").await;
526
    assert_eq!(
527
        blocked.status(),
528
        StatusCode::NETWORK_AUTHENTICATION_REQUIRED
529
    );
530
    let accepted = send(&app, "POST", "/runs/cafebabe/portal", &[], "").await;
531
    assert_eq!(accepted.status(), StatusCode::SEE_OTHER);
532
    let online = send(&app, "GET", "/runs/cafebabe", &[], "").await;
533
    assert_eq!(online.status(), StatusCode::OK);
534
}
535
536
#[tokio::test]
537
async fn the_internet_never_forgets() {
538
    let app = seeded_with(Floor::Bedroom, |run| {
539
        run.filmed_at = Some(std::time::SystemTime::now());
540
    });
541
    let footage = send(&app, "GET", "/runs/cafebabe/footage", &[], "").await;
542
    assert_eq!(footage.status(), StatusCode::OK);
543
    assert_eq!(
544
        header(&footage, "cache-control"),
545
        "public, max-age=31536000, immutable"
546
    );
547
    assert!(footage.headers().contains_key(header::AGE));
548
    let delete = send(&app, "DELETE", "/runs/cafebabe/footage", &[], "").await;
549
    assert_eq!(delete.status(), StatusCode::METHOD_NOT_ALLOWED);
550
    assert!(!header(&delete, "allow").contains("DELETE"));
551
}
552
553
#[tokio::test]
554
async fn forged_ruler_cookies_are_lies() {
555
    let forged = send(
556
        &app(),
557
        "GET",
558
        "/leaderboard?sort=content-length",
559
        &[("cookie", "ruler=1")],
560
        "",
561
    )
562
    .await;
563
    assert_eq!(forged.status(), StatusCode::BAD_REQUEST);
564
}
565
566
#[tokio::test]
567
async fn listings_never_reveal_the_private_id() {
568
    let app = app();
569
    let created = send(&app, "POST", "/runs", &[], "").await;
570
    let location = header(&created, "location").to_owned();
571
    let board = text(send(&app, "GET", "/leaderboard", &[], "").await).await;
572
    let private = location.trim_start_matches("/runs/");
573
    assert!(!board.contains(private));
574
    let entry: Value = serde_json::from_str(&board).unwrap();
575
    let public = entry["entries"][0]["public_id"].as_str().unwrap();
576
    let guess = send(&app, "DELETE", &format!("/runs/{public}"), &[], "").await;
577
    assert_eq!(guess.status(), StatusCode::NOT_FOUND);
578
}
579
580
#[tokio::test]
581
async fn seeded_runs_are_unranked() {
582
    let app = app();
583
    send(&app, "POST", "/runs?seed=7", &[], "").await;
584
    let board = json(send(&app, "GET", "/leaderboard", &[], "").await).await;
585
    assert_eq!(board["entries"].as_array().unwrap().len(), 0);
586
    let all = json(send(&app, "GET", "/leaderboard?seeded=include", &[], "").await).await;
587
    assert_eq!(all["entries"][0]["ranked"], false);
588
}
589
590
#[tokio::test]
591
async fn idle_runs_are_evicted_and_the_server_has_a_limit() {
592
    let forgetful = crate::app(Config {
593
        idle_ttl: Duration::ZERO,
594
        ..Config::default()
595
    });
596
    let first = send(&forgetful, "POST", "/runs", &[], "").await;
597
    let location = header(&first, "location").to_owned();
598
    send(&forgetful, "POST", "/runs", &[], "").await;
599
    let gone = send(&forgetful, "GET", &location, &[], "").await;
600
    assert_eq!(gone.status(), StatusCode::NOT_FOUND);
601
602
    let tiny = crate::app(Config {
603
        max_runs: 1,
604
        ..Config::default()
605
    });
606
    send(&tiny, "POST", "/runs", &[], "").await;
607
    let full = send(&tiny, "POST", "/runs", &[], "").await;
608
    assert_eq!(full.status(), StatusCode::SERVICE_UNAVAILABLE);
609
}
610
611
#[tokio::test]
612
async fn shame_from_two_runs_adds_up() {
613
    let state = Arc::new(AppState::default());
614
    state.insert(fixture());
615
    state.insert(Run::new("deadbeef".into(), "dead".into(), 7));
616
    let app = router(state);
617
    let first = send(&app, "TRACE", "/runs/cafebabe", &[], "").await;
618
    assert_eq!(header(&first, "x-shame"), "1");
619
    let cookie = header(&first, "set-cookie")
620
        .split(';')
621
        .next()
622
        .unwrap()
623
        .to_owned();
624
    let second = send(&app, "TRACE", "/runs/deadbeef", &[("cookie", &cookie)], "").await;
625
    assert_eq!(header(&second, "x-shame"), "2");
626
}
627
628
#[tokio::test]
629
async fn the_root_negotiates() {
630
    let app = app();
631
    let browser = send(&app, "GET", "/", &[("accept", "text/html,*/*")], "").await;
632
    assert_eq!(browser.status(), StatusCode::SEE_OTHER);
633
    assert_eq!(header(&browser, "location"), "/ui/");
634
    let curl = send(&app, "GET", "/", &[("accept", "*/*")], "").await;
635
    assert_eq!(json(curl).await["ui"], "/ui/");
636
    let page = send(&app, "GET", "/ui/", &[], "").await;
637
    assert_eq!(page.status(), StatusCode::OK);
638
}
639
640
#[tokio::test]
641
async fn the_ui_replays_what_browsers_cannot_send() {
642
    let app = app();
643
    let envelope =
644
        r#"{"method": "TRACE", "path": "/graveyard", "headers": [["X-Confession", "I did it"]]}"#;
645
    let traced = json(send(&app, "POST", "/ui/send", &[], envelope).await).await;
646
    assert_eq!(traced["status"], 405);
647
    let envelope = r#"{"method": "GET", "path": "/", "headers": [["Accept", "text/html"]]}"#;
648
    let redirected = json(send(&app, "POST", "/ui/send", &[], envelope).await).await;
649
    assert_eq!(redirected["status"], 303);
650
    assert_eq!(redirected["reason"], "See Other");
651
}
652
653
#[tokio::test]
654
async fn the_ui_decodes_gzip_for_display() {
655
    let app = seeded_with(Floor::Bedroom, |run| run.items.ring = true);
656
    let envelope =
657
        r#"{"method": "GET", "path": "/runs/cafebabe", "headers": [["Accept-Encoding", "gzip"]]}"#;
658
    let shown = json(send(&app, "POST", "/ui/send", &[], envelope).await).await;
659
    assert!(
660
        shown["body"]
661
            .as_str()
662
            .unwrap()
663
            .contains("\"public_id\":\"cafe\"")
664
    );
665
    let encoding = shown["headers"]
666
        .as_array()
667
        .unwrap()
668
        .iter()
669
        .any(|h| h[0] == "content-encoding" && h[1] == "gzip");
670
    assert!(encoding);
671
}
672
673
#[tokio::test]
674
async fn http_1_0_gets_the_pdf_treatment() {
675
    let request = Request::builder()
676
        .uri("/graveyard")
677
        .version(Version::HTTP_10)
678
        .body(Body::empty())
679
        .unwrap();
680
    let response = app().oneshot(request).await.unwrap();
681
    assert_eq!(response.status(), StatusCode::HTTP_VERSION_NOT_SUPPORTED);
682
}