atagen

jelq

public

an http based roguelike rpg about jelqing

Star0
Watch0
4b1f5f88827de40c3cc25edaf1c8acea7192d6cf
1
ssh://brewery@pls.lobotomise.me:33/atagen/jelq.git
src/tests/floors.rsRust497 lines17.0 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
        run.body.warmed_up = true;
119
    });
120
    let response = send(&app, "GET", "/runs/cafebabe", &[], "").await;
121
    let links = header(&response, "link");
122
    assert!(links.contains("/branches/forum>; rel=\"https://jelq.dev/rels/branch\""));
123
    assert!(links.contains("/branches/close-tab>; rel=\"https://jelq.dev/rels/branch\""));
124
    assert!(links.contains("?technique=wet>"));
125
    let jelq = send(&app, "JELQ", "/runs/cafebabe?technique=edge", &[], "").await;
126
    assert_eq!(jelq.status(), StatusCode::MULTIPLE_CHOICES);
127
}
128
129
#[tokio::test]
130
async fn either_story_choice_is_reachable_during_cooldown() {
131
    for choice in ["forum", "close-tab"] {
132
        let app = seeded_with(Floor::Bathroom, |run| {
133
            run.turn = 9;
134
            run.body.warmed_up = true;
135
            run.body.eq = 45;
136
            run.body.stamina = 1;
137
            run.items.stores.door_locks = 1;
138
        });
139
        let tenth = send(&app, "JELQ", "/runs/cafebabe?technique=edge", &[], "").await;
140
        assert_eq!(tenth.status(), StatusCode::OK);
141
        let response = send(&app, "GET", "/runs/cafebabe", &[], "").await;
142
        assert!(!header(&response, "link").contains("?technique="));
143
        let path = format!("/runs/cafebabe/branches/{choice}");
144
        assert!(
145
            header(&response, "link")
146
                .contains(&format!("<{path}>; rel=\"https://jelq.dev/rels/branch\""))
147
        );
148
        let chosen = send(&app, "POST", &path, &[], "").await;
149
        assert_eq!(chosen.status(), StatusCode::SEE_OTHER);
150
        assert!(!header(&chosen, "link").contains("/branches/"));
151
        let repeated = send(&app, "POST", &path, &[], "").await;
152
        assert_eq!(repeated.status(), StatusCode::CONFLICT);
153
    }
154
}
155
156
#[tokio::test]
157
async fn the_thread_announces_itself_at_the_branch() {
158
    let app = seeded_with(Floor::Bathroom, |run| {
159
        run.turn = 8;
160
        run.body.warmed_up = true;
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("/forum/threads/2009"));
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("/forum/threads/2009"));
189
    let announced = send(&app, "GET", "/runs/cafebabe", &[], "").await;
190
    assert!(header(&announced, "link").contains("/forum/threads/2009"));
191
}
192
193
#[tokio::test]
194
async fn rereading_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.body.warmed_up = true;
200
            run.online.story = Story::ClosedTab;
201
            run.online.meditation_taught = true;
202
        });
203
        let mut taught = false;
204
        for _ in 0..2 {
205
            let read = send(&app, "POST", "/runs/cafebabe/re-read", &[], "").await;
206
            match read.status() {
207
                StatusCode::NOT_MODIFIED => {
208
                    assert!(header(&read, "warning").contains("2009"));
209
                }
210
                StatusCode::OK => {
211
                    assert!(header(&read, "location").contains("/techniques"));
212
                    taught = true;
213
                    break;
214
                }
215
                other => panic!("seed {seed} read the thread into {other}"),
216
            }
217
        }
218
        let state = send(&app, "GET", "/runs/cafebabe", &[], "").await;
219
        assert_eq!(header(&state, "x-stamina"), "10/10", "reading is free");
220
        if taught {
221
            taught_runs += 1;
222
            let jelq = send(&app, "JELQ", "/runs/cafebabe?technique=uli", &[], "").await;
223
            assert_ne!(jelq.status(), StatusCode::FORBIDDEN);
224
        } else {
225
            let cached = send(&app, "POST", "/runs/cafebabe/re-read", &[], "").await;
226
            assert_eq!(cached.status(), StatusCode::OK);
227
            assert!(header(&cached, "warning").starts_with("112"));
228
            assert!(header(&cached, "age").parse::<u64>().unwrap() > 0);
229
        }
230
    }
231
    assert!(
232
        (30..=58).contains(&taught_runs),
233
        "{taught_runs}/100 learned uli in two reads"
234
    );
235
}
236
237
#[tokio::test]
238
async fn the_thread_cannot_be_reread_before_anyone_mentions_it() {
239
    let app = seeded_with(Floor::Bedroom, |run| run.body.warmed_up = true);
240
    let read = send(&app, "POST", "/runs/cafebabe/re-read", &[], "").await;
241
    assert_eq!(read.status(), StatusCode::NOT_FOUND);
242
    let after = send(&app, "GET", "/runs/cafebabe", &[], "").await;
243
    assert!(!header(&after, "link").contains("/re-read"));
244
}
245
246
#[tokio::test]
247
async fn rummaging_for_the_key_leaves_the_drawer_unsearched() {
248
    let mut found = 0;
249
    for seed in 0..20u64 {
250
        let app = app_with(Floor::Hotel, seed, |run| {
251
            run.here.wifi_accepted = true;
252
            run.items.kit.cage = true;
253
        });
254
        for _ in 0..30 {
255
            let rummage = send(&app, "POST", "/runs/cafebabe/drawer", &[], "").await;
256
            if rummage.status() == StatusCode::OK {
257
                break;
258
            }
259
        }
260
        let search = send(&app, "POST", "/runs/cafebabe/drawer", &[], "").await;
261
        if search.status() == StatusCode::FOUND {
262
            assert!(header(&search, "location").ends_with("/inventory/do-not-disturb-sign"));
263
            found += 1;
264
        }
265
    }
266
    assert!(
267
        found >= 18,
268
        "only {found} of 20 hotel drawers survived the rummage"
269
    );
270
}
271
272
#[tokio::test]
273
async fn kev_is_the_bar_in_the_changing_room() {
274
    for (length, shamed) in [(5.0, true), (7.0, false)] {
275
        let app = seeded_with(Floor::Bathroom, |run| {
276
            run.body.length = length;
277
            run.here.turns = 6;
278
        });
279
        let left = send(&app, "POST", "/runs/cafebabe/floor", &[], "").await;
280
        assert_eq!(header(&left, "x-floor"), "3");
281
        let warning = left
282
            .headers()
283
            .get("warning")
284
            .map(|w| w.to_str().unwrap().to_owned());
285
        assert_eq!(
286
            warning.is_some_and(|w| w.contains("Kev")),
287
            shamed,
288
            "length {length}"
289
        );
290
    }
291
}
292
293
#[tokio::test]
294
async fn pump_bro_charges_the_thin_more_for_girth() {
295
    for (girth, ring, oil) in [(4.5, 11, 4), (5.2, 8, 4)] {
296
        let app = seeded_with(Floor::ChangingRoom, |run| run.body.girth = girth);
297
        let shop = json(send(&app, "GET", "/runs/cafebabe/shop", &[], "").await).await;
298
        let price = |slug: &str| {
299
            shop["stock"]
300
                .as_array()
301
                .unwrap()
302
                .iter()
303
                .find(|item| item["item"] == slug)
304
                .unwrap()["price"]
305
                .as_u64()
306
                .unwrap()
307
        };
308
        assert_eq!(price("cock-ring"), ring, "girth {girth}");
309
        assert_eq!(price("baby-oil"), oil, "girth {girth}");
310
    }
311
}
312
313
#[tokio::test]
314
async fn the_clinic_escalates_toward_honesty() {
315
    let Harness { app, .. } = harness(Floor::Clinic, SEED, |_| {});
316
    let mut hints = Vec::new();
317
    for _ in 0..3 {
318
        let response = send(
319
            &app,
320
            "GET",
321
            "/runs/cafebabe/diagnosis",
322
            &[("authorization", "Excuse \"changing\"")],
323
            "",
324
        )
325
        .await;
326
        assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
327
        hints.push(header(&response, "www-authenticate").to_owned());
328
    }
329
    assert!(hints[2].contains("Be honest."));
330
}
331
332
#[tokio::test]
333
async fn the_true_ending_is_not_modified() {
334
    let Harness { app: early, .. } = harness(Floor::Bedroom, SEED, |_| {});
335
    let response = send(&early, "GET", "/runs/cafebabe/diagnosis", &[], "").await;
336
    assert_eq!(response.status(), StatusCode::TOO_EARLY);
337
338
    let Harness {
339
        app, origin: etag, ..
340
    } = harness(Floor::Clinic, SEED, |_| {});
341
    assert!(
342
        !etag.contains("cafebabe"),
343
        "the origin ETag must not be derivable from the id"
344
    );
345
    let response = send(
346
        &app,
347
        "GET",
348
        "/runs/cafebabe/diagnosis",
349
        &[
350
            ("authorization", "Bearer honesty"),
351
            ("if-none-match", &etag),
352
        ],
353
        "",
354
    )
355
    .await;
356
    assert_eq!(response.status(), StatusCode::NOT_MODIFIED);
357
    assert_eq!(header(&response, "x-diagnosis"), "normal");
358
}
359
360
#[tokio::test]
361
async fn the_elder_bans_three_lies_unless_the_girth_impresses() {
362
    let forum = |girth: f64| {
363
        seeded_with(Floor::Forum, move |run| {
364
            run.body.length = 6.0;
365
            run.body.girth = girth;
366
        })
367
    };
368
    let fib = [("proxy-authorization", "Ruler 6.5")];
369
    let thin = forum(4.5);
370
    for _ in 0..2 {
371
        let strike = send(&thin, "GET", "/runs/cafebabe/elder", &fib, "").await;
372
        assert_eq!(strike.status(), StatusCode::PROXY_AUTHENTICATION_REQUIRED);
373
    }
374
    let banned = send(&thin, "GET", "/runs/cafebabe/elder", &fib, "").await;
375
    assert_eq!(banned.status(), StatusCode::FORBIDDEN);
376
    assert!(
377
        json(banned).await["type"]
378
            .as_str()
379
            .unwrap()
380
            .ends_with("banned")
381
    );
382
383
    let thick = forum(5.2);
384
    for _ in 0..4 {
385
        let excused = send(&thick, "GET", "/runs/cafebabe/elder", &fib, "").await;
386
        assert_eq!(excused.status(), StatusCode::PROXY_AUTHENTICATION_REQUIRED);
387
        assert!(header(&excused, "x-forum-note").contains("Girth is also a dimension"));
388
    }
389
}
390
391
#[tokio::test]
392
async fn the_zip_ends_one_exit_in_two_hundred() {
393
    let mut zipped = 0;
394
    for seed in 0..4000u64 {
395
        let mut run = Run::new("cafebabe".into(), "cafe".into(), seed);
396
        run.here.turns = 6;
397
        if run.advance().is_err() && run.ended.is_some() {
398
            zipped += 1;
399
        }
400
    }
401
    assert!(
402
        (8..=35).contains(&zipped),
403
        "{zipped} zips in 4000 exits, about 20 expected"
404
    );
405
}
406
407
#[tokio::test]
408
async fn a_prescription_ends_the_run_and_the_true_ending_still_waits() {
409
    let Harness { app, origin, .. } = harness(Floor::Clinic, SEED, |_| {});
410
    let honest = [("authorization", "Bearer honesty")];
411
    let prescribed = send(&app, "GET", "/runs/cafebabe/diagnosis", &honest, "").await;
412
    assert_eq!(prescribed.status(), StatusCode::OK);
413
    assert!(header(&prescribed, "server-timing").contains("volume;"));
414
    assert!(
415
        json(prescribed).await["detail"]
416
            .as_str()
417
            .unwrap()
418
            .starts_with("Congratulations")
419
    );
420
    let board = json(send(&app, "GET", "/leaderboard?seeded=include", &[], "").await).await;
421
    assert_eq!(board["entries"][0]["status"], "prescribed");
422
423
    let conditional = [
424
        ("authorization", "Bearer honesty"),
425
        ("if-none-match", origin.as_str()),
426
    ];
427
    let cured = send(&app, "GET", "/runs/cafebabe/diagnosis", &conditional, "").await;
428
    assert_eq!(cured.status(), StatusCode::NOT_MODIFIED);
429
    let view = json(send(&app, "GET", "/runs/cafebabe", &[], "").await).await;
430
    assert_eq!(view["status"], "normal");
431
}
432
433
#[tokio::test]
434
async fn the_foreskin_window_has_hard_edges() {
435
    for (foreskin, refused) in [
436
        (0.79, Some(StatusCode::LENGTH_REQUIRED)),
437
        (0.8, None),
438
        (2.0, None),
439
        (2.01, Some(StatusCode::PAYLOAD_TOO_LARGE)),
440
    ] {
441
        let app = seeded_with(Floor::Hotel, |run| {
442
            run.here.wifi_accepted = true;
443
            run.body.warmed_up = true;
444
            run.body.eq = 55;
445
            run.body.foreskin = Some(foreskin);
446
        });
447
        let dry = send(&app, "JELQ", "/runs/cafebabe?technique=dry", &[], "").await;
448
        match refused {
449
            Some(status) => assert_eq!(dry.status(), status, "foreskin {foreskin}"),
450
            None => assert!(
451
                ![StatusCode::LENGTH_REQUIRED, StatusCode::PAYLOAD_TOO_LARGE]
452
                    .contains(&dry.status()),
453
                "foreskin {foreskin} sits inside the window"
454
            ),
455
        }
456
    }
457
}
458
459
#[tokio::test]
460
async fn the_forum_teaches_breathing_before_uli() {
461
    let mut breathing = 0;
462
    for seed in 0..100u64 {
463
        let app = app_with(Floor::Bedroom, seed, |run| {
464
            run.turn = BRANCH_TURN;
465
            run.body.warmed_up = true;
466
            run.online.story = Story::Pending;
467
        });
468
        let listed = json(send(&app, "GET", "/runs/cafebabe/techniques", &[], "").await).await;
469
        let meditate = listed["techniques"]
470
            .as_array()
471
            .unwrap()
472
            .iter()
473
            .find(|t| t["name"] == "meditate")
474
            .unwrap();
475
        assert_eq!(meditate["locked"], true);
476
        send(&app, "POST", "/runs/cafebabe/branches/forum", &[], "").await;
477
        for _ in 0..2 {
478
            send(&app, "POST", "/runs/cafebabe/re-read", &[], "").await;
479
        }
480
        let meditated = send(&app, "JELQ", "/runs/cafebabe?technique=meditate", &[], "").await;
481
        if meditated.status() == StatusCode::OK {
482
            breathing += 1;
483
        } else {
484
            assert_eq!(meditated.status(), StatusCode::FORBIDDEN);
485
            let uli = send(&app, "JELQ", "/runs/cafebabe?technique=uli", &[], "").await;
486
            assert_eq!(
487
                uli.status(),
488
                StatusCode::FORBIDDEN,
489
                "seed {seed} skipped ahead"
490
            );
491
        }
492
    }
493
    assert!(
494
        (75..=97).contains(&breathing),
495
        "{breathing}/100 learned to breathe on the branch floor"
496
    );
497
}