atagen

jelq

public

an http based roguelike rpg about jelqing

Star0
Watch0
53bce0d930007b21fb1f130865979bbd959edcc5
1
ssh://brewery@pls.lobotomise.me:33/atagen/jelq.git
src/tests/floors.rsRust543 lines18.5 KB
1
use super::*;
2
3
#[tokio::test]
4
async fn only_the_key_opens_the_cage() {
5
    let app = seeded_with(Floor::ChangingRoom, |run| {
6
        run.items.kit.cage = true;
7
        run.standing.cash = 30;
8
    });
9
    send(&app, "POST", "/runs/cafebabe/shop/ice-pack", &[], "").await;
10
    let still = send(
11
        &app,
12
        "GET",
13
        "/runs/cafebabe/inventory/chastity-cage",
14
        &[],
15
        "",
16
    )
17
    .await;
18
    assert_eq!(still.status(), StatusCode::OK);
19
    send(&app, "POST", "/runs/cafebabe/shop/cage-key", &[], "").await;
20
    let gone = send(
21
        &app,
22
        "GET",
23
        "/runs/cafebabe/inventory/chastity-cage",
24
        &[],
25
        "",
26
    )
27
    .await;
28
    assert_eq!(gone.status(), StatusCode::NOT_FOUND);
29
}
30
31
#[tokio::test]
32
async fn pump_bro_upsells() {
33
    let closed = seeded_with(Floor::Bedroom, |_| {});
34
    let response = send(&closed, "GET", "/runs/cafebabe/shop", &[], "").await;
35
    assert_eq!(response.status(), StatusCode::NOT_FOUND);
36
37
    let app = seeded_with(Floor::ChangingRoom, |_| {});
38
    let pump = send(&app, "POST", "/runs/cafebabe/shop/pump", &[], "").await;
39
    assert_eq!(pump.status(), StatusCode::UPGRADE_REQUIRED);
40
    assert_eq!(header(&pump, "upgrade"), "pump-pro/2.0");
41
    let pro = send(&app, "POST", "/runs/cafebabe/shop/pump-pro", &[], "").await;
42
    assert_eq!(pro.status(), StatusCode::CREATED);
43
    let broke = send(&app, "POST", "/runs/cafebabe/shop/extender", &[], "").await;
44
    assert_eq!(broke.status(), StatusCode::PAYMENT_REQUIRED);
45
}
46
47
#[tokio::test]
48
async fn the_elder_is_stale() {
49
    let app = seeded_with(Floor::Forum, |run| run.body.length = 5.0);
50
    let unverified = send(&app, "GET", "/runs/cafebabe/elder", &[], "").await;
51
    assert_eq!(
52
        unverified.status(),
53
        StatusCode::PROXY_AUTHENTICATION_REQUIRED
54
    );
55
    let lie = send(
56
        &app,
57
        "GET",
58
        "/runs/cafebabe/elder",
59
        &[("proxy-authorization", "Ruler 9")],
60
        "",
61
    )
62
    .await;
63
    assert_eq!(lie.status(), StatusCode::BAD_REQUEST);
64
    let csv = send(
65
        &app,
66
        "GET",
67
        "/runs/cafebabe/elder",
68
        &[("proxy-authorization", "Ruler 5.0")],
69
        "",
70
    )
71
    .await;
72
    assert_eq!(csv.status(), StatusCode::OK);
73
    let stale = send(
74
        &app,
75
        "GET",
76
        "/runs/cafebabe/elder",
77
        &[("if-modified-since", "Mon, 15 Jun 2009 00:00:00 GMT")],
78
        "",
79
    )
80
    .await;
81
    assert_eq!(stale.status(), StatusCode::NOT_MODIFIED);
82
}
83
84
#[tokio::test]
85
async fn the_hotel_has_a_captive_portal() {
86
    let app = seeded_with(Floor::Hotel, |_| {});
87
    let blocked = send(&app, "GET", "/runs/cafebabe", &[], "").await;
88
    assert_eq!(
89
        blocked.status(),
90
        StatusCode::NETWORK_AUTHENTICATION_REQUIRED
91
    );
92
    let accepted = send(&app, "POST", "/runs/cafebabe/portal", &[], "").await;
93
    assert_eq!(accepted.status(), StatusCode::SEE_OTHER);
94
    let online = send(&app, "GET", "/runs/cafebabe", &[], "").await;
95
    assert_eq!(online.status(), StatusCode::OK);
96
}
97
98
#[tokio::test]
99
async fn caged_drawers_are_noisy_and_eventually_give_up_the_key() {
100
    let app = seeded_with(Floor::ChangingRoom, |run| run.items.kit.cage = true);
101
    let mut statuses = Vec::new();
102
    for _ in 0..30 {
103
        let response = send(&app, "POST", "/runs/cafebabe/drawer", &[], "").await;
104
        statuses.push(response.status());
105
        if response.status() == StatusCode::OK {
106
            break;
107
        }
108
    }
109
    assert_eq!(statuses.last(), Some(&StatusCode::OK), "{statuses:?}");
110
    assert!(
111
        statuses[..statuses.len() - 1]
112
            .iter()
113
            .all(|s| *s == StatusCode::NO_CONTENT)
114
    );
115
}
116
117
#[tokio::test]
118
async fn an_open_tab_is_linked_and_jelq_still_answers_with_choices() {
119
    let app = seeded_with(Floor::Bathroom, |run| {
120
        run.online.story = Story::Pending;
121
    });
122
    let response = send(&app, "GET", "/runs/cafebabe", &[], "").await;
123
    let links = header(&response, "link");
124
    assert!(links.contains("/branches/forum>; rel=\"https://jelq.dev/rels/branch\""));
125
    assert!(links.contains("/branches/close-tab>; rel=\"https://jelq.dev/rels/branch\""));
126
    assert!(links.contains("?technique=wet>"));
127
    let jelq = send(&app, "JELQ", "/runs/cafebabe?technique=edge", &[], "").await;
128
    assert_eq!(jelq.status(), StatusCode::MULTIPLE_CHOICES);
129
}
130
131
#[tokio::test]
132
async fn either_story_choice_is_reachable_during_cooldown() {
133
    for choice in ["forum", "close-tab"] {
134
        let app = seeded_with(Floor::Bathroom, |run| {
135
            run.turn = 9;
136
            run.body.eq = 45;
137
            run.body.stamina = 1;
138
            run.items.stores.door_locks = 1;
139
        });
140
        let tenth = send(&app, "JELQ", "/runs/cafebabe?technique=edge", &[], "").await;
141
        assert_eq!(tenth.status(), StatusCode::OK);
142
        let response = send(&app, "GET", "/runs/cafebabe", &[], "").await;
143
        assert!(!header(&response, "link").contains("?technique="));
144
        let path = format!("/runs/cafebabe/branches/{choice}");
145
        assert!(
146
            header(&response, "link")
147
                .contains(&format!("<{path}>; rel=\"https://jelq.dev/rels/branch\""))
148
        );
149
        let chosen = send(&app, "POST", &path, &[], "").await;
150
        assert_eq!(chosen.status(), StatusCode::SEE_OTHER);
151
        assert!(!header(&chosen, "link").contains("/branches/"));
152
        let repeated = send(&app, "POST", &path, &[], "").await;
153
        assert_eq!(repeated.status(), StatusCode::CONFLICT);
154
    }
155
}
156
157
#[tokio::test]
158
async fn the_thread_announces_itself_at_the_branch() {
159
    let app = seeded_with(Floor::Bathroom, |run| {
160
        run.turn = 8;
161
        run.items.lubes.baby_oil = 9;
162
    });
163
    send(
164
        &app,
165
        "JELQ",
166
        "/runs/cafebabe?technique=wet",
167
        &[("content-type", "application/baby-oil")],
168
        "",
169
    )
170
    .await;
171
    let ninth = send(&app, "GET", "/runs/cafebabe", &[], "").await;
172
    assert!(!header(&ninth, "link").contains("/cafebabe/thread>"));
173
    let tenth = send(
174
        &app,
175
        "JELQ",
176
        "/runs/cafebabe?technique=wet",
177
        &[("content-type", "application/baby-oil")],
178
        "",
179
    )
180
    .await;
181
    assert!(
182
        tenth
183
            .headers()
184
            .get_all("warning")
185
            .iter()
186
            .any(|w| w.to_str().unwrap().contains("a forum thread from 2009"))
187
    );
188
    assert!(header(&tenth, "link").contains("/cafebabe/thread>"));
189
    let announced = send(&app, "GET", "/runs/cafebabe", &[], "").await;
190
    assert!(header(&announced, "link").contains("/cafebabe/thread>"));
191
}
192
193
#[tokio::test]
194
async fn reading_the_thread_teaches_uli() {
195
    let mut taught_runs = 0;
196
    for seed in 0..100u64 {
197
        let app = app_with(Floor::Bedroom, seed, |run| {
198
            run.turn = BRANCH_TURN;
199
            run.online.story = Story::ClosedTab;
200
            run.online.meditation_taught = true;
201
        });
202
        let mut taught = false;
203
        for _ in 0..2 {
204
            let read = send(&app, "GET", "/runs/cafebabe/thread", &[], "").await;
205
            assert_eq!(read.status(), StatusCode::OK, "seed {seed}");
206
            assert!(warnings(&read).is_empty(), "a live read gives nothing away");
207
            let body = json(read).await;
208
            assert!(body["posts"].is_array(), "a read always shows the thread");
209
            if body["detail"].is_string() {
210
                taught = true;
211
                break;
212
            }
213
        }
214
        let state = send(&app, "GET", "/runs/cafebabe", &[], "").await;
215
        assert_eq!(header(&state, "x-stamina"), "10/10", "reading is free");
216
        if taught {
217
            taught_runs += 1;
218
            let jelq = send(&app, "JELQ", "/runs/cafebabe?technique=uli", &[], "").await;
219
            assert_ne!(jelq.status(), StatusCode::FORBIDDEN);
220
        } else {
221
            let cached = send(&app, "GET", "/runs/cafebabe/thread", &[], "").await;
222
            assert_eq!(cached.status(), StatusCode::OK);
223
            assert!(header(&cached, "warning").starts_with("112"));
224
            assert!(header(&cached, "age").parse::<u64>().unwrap() > 0);
225
        }
226
    }
227
    assert!(
228
        (30..=58).contains(&taught_runs),
229
        "{taught_runs}/100 learned uli in two reads"
230
    );
231
}
232
233
#[tokio::test]
234
async fn the_forum_may_print_you_a_ruler() {
235
    let mut printed = 0;
236
    for seed in 0..100u64 {
237
        let app = app_with(Floor::Forum, seed, |run| {
238
            run.online.story = Story::ReadForum;
239
            run.online.meditation_taught = true;
240
            run.online.uli_taught = true;
241
        });
242
        for _ in 0..2 {
243
            let read = send(&app, "GET", "/runs/cafebabe/thread", &[], "").await;
244
            if read
245
                .headers()
246
                .get("location")
247
                .is_some_and(|l| l.to_str().unwrap().ends_with("/inventory/ruler"))
248
            {
249
                printed += 1;
250
                let state = send(&app, "GET", "/runs/cafebabe", &[], "").await;
251
                assert!(!header(&state, "x-length").is_empty());
252
                break;
253
            }
254
        }
255
    }
256
    assert!(
257
        (40..=72).contains(&printed),
258
        "{printed}/100 printed a ruler in two reads"
259
    );
260
261
    let measured = seeded_with(Floor::Forum, |run| {
262
        run.online.story = Story::ReadForum;
263
        run.online.uli_taught = true;
264
        run.items.kit.ruler = true;
265
    });
266
    for _ in 0..2 {
267
        let read = send(&measured, "GET", "/runs/cafebabe/thread", &[], "").await;
268
        assert!(read.headers().get("location").is_none());
269
    }
270
}
271
272
#[tokio::test]
273
async fn the_thread_cannot_be_read_before_anyone_mentions_it() {
274
    let app = seeded_with(Floor::Bedroom, |_| {});
275
    let read = send(&app, "GET", "/runs/cafebabe/thread", &[], "").await;
276
    assert_eq!(read.status(), StatusCode::NOT_FOUND);
277
    let after = send(&app, "GET", "/runs/cafebabe", &[], "").await;
278
    assert!(!header(&after, "link").contains("/thread"));
279
}
280
281
#[tokio::test]
282
async fn rummaging_for_the_key_leaves_the_drawer_unsearched() {
283
    let mut found = 0;
284
    for seed in 0..20u64 {
285
        let app = app_with(Floor::Hotel, seed, |run| {
286
            run.here.wifi_accepted = true;
287
            run.items.kit.cage = true;
288
        });
289
        for _ in 0..30 {
290
            let rummage = send(&app, "POST", "/runs/cafebabe/drawer", &[], "").await;
291
            if rummage.status() == StatusCode::OK {
292
                break;
293
            }
294
        }
295
        let search = send(&app, "POST", "/runs/cafebabe/drawer", &[], "").await;
296
        if search.status() == StatusCode::FOUND {
297
            assert!(header(&search, "location").ends_with("/inventory/do-not-disturb-sign"));
298
            found += 1;
299
        }
300
    }
301
    assert!(
302
        found >= 18,
303
        "only {found} of 20 hotel drawers survived the rummage"
304
    );
305
}
306
307
#[tokio::test]
308
async fn kev_is_the_bar_in_the_changing_room() {
309
    for (length, shamed) in [(5.0, true), (7.0, false)] {
310
        let app = seeded_with(Floor::Bathroom, |run| {
311
            run.body.length = length;
312
            run.here.turns = 6;
313
        });
314
        let left = send(&app, "POST", "/runs/cafebabe/floor", &[], "").await;
315
        assert_eq!(header(&left, "x-floor"), "3");
316
        let warning = left
317
            .headers()
318
            .get("warning")
319
            .map(|w| w.to_str().unwrap().to_owned());
320
        assert_eq!(
321
            warning.is_some_and(|w| w.contains("Kev")),
322
            shamed,
323
            "length {length}"
324
        );
325
    }
326
}
327
328
#[tokio::test]
329
async fn pump_bro_charges_the_thin_more_for_girth() {
330
    for (girth, ring, oil) in [(4.5, 11, 4), (5.2, 8, 4)] {
331
        let app = seeded_with(Floor::ChangingRoom, |run| run.body.girth = girth);
332
        let shop = json(send(&app, "GET", "/runs/cafebabe/shop", &[], "").await).await;
333
        let price = |slug: &str| {
334
            shop["stock"]
335
                .as_array()
336
                .unwrap()
337
                .iter()
338
                .find(|item| item["item"] == slug)
339
                .unwrap()["price"]
340
                .as_u64()
341
                .unwrap()
342
        };
343
        assert_eq!(price("cock-ring"), ring, "girth {girth}");
344
        assert_eq!(price("baby-oil"), oil, "girth {girth}");
345
    }
346
}
347
348
#[tokio::test]
349
async fn the_clinic_escalates_toward_honesty() {
350
    let Harness { app, .. } = harness(Floor::Clinic, SEED, |_| {});
351
    let mut hints = Vec::new();
352
    for _ in 0..3 {
353
        let response = send(
354
            &app,
355
            "GET",
356
            "/runs/cafebabe/diagnosis",
357
            &[("authorization", "Excuse \"changing\"")],
358
            "",
359
        )
360
        .await;
361
        assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
362
        hints.push(header(&response, "www-authenticate").to_owned());
363
    }
364
    assert!(hints[2].contains("Be honest."));
365
}
366
367
#[tokio::test]
368
async fn the_true_ending_is_not_modified() {
369
    let Harness { app: early, .. } = harness(Floor::Bedroom, SEED, |_| {});
370
    let response = send(&early, "GET", "/runs/cafebabe/diagnosis", &[], "").await;
371
    assert_eq!(response.status(), StatusCode::TOO_EARLY);
372
373
    let Harness {
374
        app, origin: etag, ..
375
    } = harness(Floor::Clinic, SEED, |_| {});
376
    assert!(
377
        !etag.contains("cafebabe"),
378
        "the origin ETag must not be derivable from the id"
379
    );
380
    let response = send(
381
        &app,
382
        "GET",
383
        "/runs/cafebabe/diagnosis",
384
        &[
385
            ("authorization", "Bearer honesty"),
386
            ("if-none-match", &etag),
387
        ],
388
        "",
389
    )
390
    .await;
391
    assert_eq!(response.status(), StatusCode::NOT_MODIFIED);
392
    assert_eq!(header(&response, "x-diagnosis"), "normal");
393
}
394
395
#[tokio::test]
396
async fn the_elder_bans_three_lies_unless_the_girth_impresses() {
397
    let forum = |girth: f64| {
398
        seeded_with(Floor::Forum, move |run| {
399
            run.body.length = 6.0;
400
            run.body.girth = girth;
401
        })
402
    };
403
    let fib = [("proxy-authorization", "Ruler 6.5")];
404
    let thin = forum(4.5);
405
    for _ in 0..2 {
406
        let strike = send(&thin, "GET", "/runs/cafebabe/elder", &fib, "").await;
407
        assert_eq!(strike.status(), StatusCode::PROXY_AUTHENTICATION_REQUIRED);
408
    }
409
    let banned = send(&thin, "GET", "/runs/cafebabe/elder", &fib, "").await;
410
    assert_eq!(banned.status(), StatusCode::FORBIDDEN);
411
    assert!(
412
        json(banned).await["type"]
413
            .as_str()
414
            .unwrap()
415
            .ends_with("banned")
416
    );
417
418
    let thick = forum(5.2);
419
    for _ in 0..4 {
420
        let excused = send(&thick, "GET", "/runs/cafebabe/elder", &fib, "").await;
421
        assert_eq!(excused.status(), StatusCode::PROXY_AUTHENTICATION_REQUIRED);
422
        assert!(header(&excused, "x-forum-note").contains("Girth is also a dimension"));
423
    }
424
}
425
426
#[tokio::test]
427
async fn the_zip_ends_one_exit_in_two_hundred() {
428
    let mut zipped = 0;
429
    for seed in 0..4000u64 {
430
        let mut run = Run::new("cafebabe".into(), "cafe".into(), seed);
431
        run.here.turns = 6;
432
        if run.advance().is_err() && run.ended.is_some() {
433
            zipped += 1;
434
        }
435
    }
436
    assert!(
437
        (8..=35).contains(&zipped),
438
        "{zipped} zips in 4000 exits, about 20 expected"
439
    );
440
}
441
442
#[tokio::test]
443
async fn a_prescription_ends_the_run_and_the_true_ending_still_waits() {
444
    let Harness { app, origin, .. } = harness(Floor::Clinic, SEED, |_| {});
445
    let honest = [("authorization", "Bearer honesty")];
446
    let prescribed = send(&app, "GET", "/runs/cafebabe/diagnosis", &honest, "").await;
447
    assert_eq!(prescribed.status(), StatusCode::OK);
448
    assert!(header(&prescribed, "server-timing").contains("volume;"));
449
    assert!(
450
        json(prescribed).await["detail"]
451
            .as_str()
452
            .unwrap()
453
            .starts_with("Congratulations")
454
    );
455
    let board = json(send(&app, "GET", "/leaderboard?seeded=include", &[], "").await).await;
456
    assert_eq!(board["entries"][0]["status"], "prescribed");
457
458
    let conditional = [
459
        ("authorization", "Bearer honesty"),
460
        ("if-none-match", origin.as_str()),
461
    ];
462
    let cured = send(&app, "GET", "/runs/cafebabe/diagnosis", &conditional, "").await;
463
    assert_eq!(cured.status(), StatusCode::NOT_MODIFIED);
464
    let view = json(send(&app, "GET", "/runs/cafebabe", &[], "").await).await;
465
    assert_eq!(view["status"], "normal");
466
}
467
468
#[tokio::test]
469
async fn the_foreskin_window_has_hard_edges() {
470
    for (foreskin, refused) in [
471
        (0.79, Some(StatusCode::LENGTH_REQUIRED)),
472
        (0.8, None),
473
        (2.0, None),
474
        (2.01, Some(StatusCode::PAYLOAD_TOO_LARGE)),
475
    ] {
476
        let app = seeded_with(Floor::Hotel, |run| {
477
            run.here.wifi_accepted = true;
478
            run.body.eq = 55;
479
            run.body.foreskin = Some(foreskin);
480
        });
481
        let dry = send(&app, "JELQ", "/runs/cafebabe?technique=dry", &[], "").await;
482
        match refused {
483
            Some(status) => assert_eq!(dry.status(), status, "foreskin {foreskin}"),
484
            None => assert!(
485
                ![StatusCode::LENGTH_REQUIRED, StatusCode::PAYLOAD_TOO_LARGE]
486
                    .contains(&dry.status()),
487
                "foreskin {foreskin} sits inside the window"
488
            ),
489
        }
490
    }
491
}
492
493
#[tokio::test]
494
async fn the_forum_teaches_breathing_before_uli() {
495
    let mut breathing = 0;
496
    for seed in 0..100u64 {
497
        let app = app_with(Floor::Bedroom, seed, |run| {
498
            run.turn = BRANCH_TURN;
499
            run.online.story = Story::Pending;
500
        });
501
        let listed = json(send(&app, "GET", "/runs/cafebabe/techniques", &[], "").await).await;
502
        let meditate = listed["techniques"]
503
            .as_array()
504
            .unwrap()
505
            .iter()
506
            .find(|t| t["name"] == "meditate")
507
            .unwrap();
508
        assert_eq!(meditate["locked"], true);
509
        send(&app, "POST", "/runs/cafebabe/branches/forum", &[], "").await;
510
        for _ in 0..2 {
511
            send(&app, "GET", "/runs/cafebabe/thread", &[], "").await;
512
        }
513
        let meditated = send(&app, "JELQ", "/runs/cafebabe?technique=meditate", &[], "").await;
514
        if meditated.status() == StatusCode::OK {
515
            breathing += 1;
516
        } else {
517
            assert_eq!(meditated.status(), StatusCode::FORBIDDEN);
518
            let uli = send(&app, "JELQ", "/runs/cafebabe?technique=uli", &[], "").await;
519
            assert_eq!(
520
                uli.status(),
521
                StatusCode::FORBIDDEN,
522
                "seed {seed} skipped ahead"
523
            );
524
        }
525
    }
526
    assert!(
527
        (75..=97).contains(&breathing),
528
        "{breathing}/100 learned to breathe on the branch floor"
529
    );
530
}
531
532
#[tokio::test]
533
async fn a_bought_ruler_owns_up_to_the_self_report() {
534
    let app = seeded_with(Floor::ChangingRoom, |run| {
535
        run.standing.cash = 100;
536
        run.body.inflation = 1.2;
537
    });
538
    let bought = send(&app, "POST", "/runs/cafebabe/shop/ruler", &[], "").await;
539
    assert_eq!(bought.status(), StatusCode::CREATED);
540
    assert!(bought.headers().contains_key("x-length"));
541
    let detail = json(bought).await["detail"].as_str().unwrap().to_owned();
542
    assert!(detail.contains("1.2 inches generous"), "{detail}");
543
}