atagen

jelq

public

an http based roguelike rpg about jelqing

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