atagen

jelq

public

an http based roguelike rpg about jelqing

Star0
Watch0
177535ac8abf29d1d6eaa1bd35a72ae370329b5f
1
ssh://brewery@pls.lobotomise.me:33/atagen/jelq.git
tests/http.rsRust441 lines13.1 KB
1
use std::sync::Arc;
2
3
use axum::{
4
    Router,
5
    body::{Body, to_bytes},
6
    http::{Method, Request, StatusCode, header},
7
    response::Response,
8
};
9
use jelqd::{
10
    AppState,
11
    game::{Floor, Run},
12
    router,
13
};
14
use serde_json::Value;
15
use tower::ServiceExt;
16
17
async fn send(
18
    app: &Router,
19
    method: &str,
20
    uri: &str,
21
    headers: &[(&str, &str)],
22
    body: &str,
23
) -> Response {
24
    let mut request = Request::builder()
25
        .method(Method::from_bytes(method.as_bytes()).unwrap())
26
        .uri(uri);
27
    for (name, value) in headers {
28
        request = request.header(*name, *value);
29
    }
30
    app.clone()
31
        .oneshot(request.body(Body::from(body.to_owned())).unwrap())
32
        .await
33
        .unwrap()
34
}
35
36
async fn json(response: Response) -> Value {
37
    serde_json::from_slice(&to_bytes(response.into_body(), usize::MAX).await.unwrap()).unwrap()
38
}
39
40
fn seeded(floor: Floor) -> (Router, String) {
41
    let state = Arc::new(AppState::default());
42
    let mut run = Run::new("cafebabe".into(), 69, 0);
43
    run.floor = floor;
44
    let etag = run.origin_etag.clone();
45
    state.insert(run);
46
    (router(state), etag)
47
}
48
49
#[tokio::test]
50
async fn creating_a_run_returns_location_and_etag() {
51
    let app = jelqd::app();
52
    let response = send(&app, "POST", "/runs?seed=1", &[], "").await;
53
    assert_eq!(response.status(), StatusCode::CREATED);
54
    assert!(response.headers().contains_key(header::LOCATION));
55
    assert!(response.headers().contains_key(header::ETAG));
56
    assert_eq!(response.headers()[header::SERVER], "jelqd/0.6.9");
57
}
58
59
#[tokio::test]
60
async fn jelqing_cold_fails_the_precondition() {
61
    let (app, _) = seeded(Floor::Bedroom);
62
    let response = send(&app, "JELQ", "/runs/cafebabe?technique=stretch", &[], "").await;
63
    assert_eq!(response.status(), StatusCode::PRECONDITION_FAILED);
64
    assert_eq!(
65
        response.headers()[header::CONTENT_TYPE],
66
        "application/problem+json"
67
    );
68
}
69
70
#[tokio::test]
71
async fn wet_without_lube_is_unsupported_media() {
72
    let (app, _) = seeded(Floor::Bedroom);
73
    send(&app, "JELQ", "/runs/cafebabe?technique=warmup", &[], "").await;
74
    let response = send(&app, "JELQ", "/runs/cafebabe?technique=wet", &[], "").await;
75
    assert_eq!(response.status(), StatusCode::UNSUPPORTED_MEDIA_TYPE);
76
    let images = send(
77
        &app,
78
        "JELQ",
79
        "/runs/cafebabe?technique=wet",
80
        &[("content-type", "image/jpeg")],
81
        "",
82
    )
83
    .await;
84
    assert_eq!(json(images).await["detail"], "No.");
85
}
86
87
#[tokio::test]
88
async fn unknown_methods_are_not_implemented() {
89
    let (app, _) = seeded(Floor::Bedroom);
90
    let response = send(&app, "MILK", "/runs/cafebabe", &[], "").await;
91
    assert_eq!(response.status(), StatusCode::NOT_IMPLEMENTED);
92
}
93
94
#[tokio::test]
95
async fn patch_only_accepts_the_truth() {
96
    let (app, _) = seeded(Floor::Bedroom);
97
    let patch = [("content-type", "application/json-patch+json")];
98
    let replace = send(
99
        &app,
100
        "PATCH",
101
        "/runs/cafebabe",
102
        &patch,
103
        r#"[{"op":"replace","path":"/length","value":12}]"#,
104
    )
105
    .await;
106
    assert_eq!(replace.status(), StatusCode::UNPROCESSABLE_ENTITY);
107
    let lie = send(
108
        &app,
109
        "PATCH",
110
        "/runs/cafebabe",
111
        &patch,
112
        r#"[{"op":"test","path":"/length","value":40}]"#,
113
    )
114
    .await;
115
    assert_eq!(
116
        json(lie).await["type"],
117
        "https://jelq.dev/problems/exaggeration"
118
    );
119
}
120
121
#[tokio::test]
122
async fn deleting_the_foreskin_requires_certainty() {
123
    let (app, _) = seeded(Floor::Bedroom);
124
    let response = send(&app, "DELETE", "/runs/cafebabe/foreskin", &[], "").await;
125
    assert_eq!(response.status(), StatusCode::PRECONDITION_REQUIRED);
126
    let etag = send(&app, "GET", "/runs/cafebabe/foreskin", &[], "")
127
        .await
128
        .headers()[header::ETAG]
129
        .to_str()
130
        .unwrap()
131
        .to_owned();
132
    let response = send(
133
        &app,
134
        "DELETE",
135
        "/runs/cafebabe/foreskin",
136
        &[("if-match", &etag)],
137
        "",
138
    )
139
    .await;
140
    assert_eq!(response.status(), StatusCode::NO_CONTENT);
141
    let response = send(&app, "GET", "/runs/cafebabe/foreskin", &[], "").await;
142
    assert_eq!(response.status(), StatusCode::GONE);
143
}
144
145
#[tokio::test]
146
async fn length_is_chunked_until_the_ruler() {
147
    let (app, _) = seeded(Floor::Bedroom);
148
    let lie = send(&app, "GET", "/runs/cafebabe/length", &[], "").await;
149
    assert!(!lie.headers().contains_key(header::CONTENT_LENGTH));
150
    let found = send(&app, "POST", "/runs/cafebabe/drawer", &[], "").await;
151
    assert_eq!(found.status(), StatusCode::FOUND);
152
    let truth = send(&app, "GET", "/runs/cafebabe/length", &[], "").await;
153
    let declared: usize = truth.headers()[header::CONTENT_LENGTH]
154
        .to_str()
155
        .unwrap()
156
        .parse()
157
        .unwrap();
158
    let body = to_bytes(truth.into_body(), usize::MAX).await.unwrap();
159
    assert_eq!(body.len(), declared);
160
    assert!(body.starts_with(b"8=") && body.ends_with(b"D"));
161
}
162
163
#[tokio::test]
164
async fn the_clinic_escalates_toward_honesty() {
165
    let (app, _) = seeded(Floor::Clinic);
166
    let mut hints = Vec::new();
167
    for _ in 0..3 {
168
        let response = send(
169
            &app,
170
            "GET",
171
            "/runs/cafebabe/diagnosis",
172
            &[("authorization", "Excuse \"changing\"")],
173
            "",
174
        )
175
        .await;
176
        assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
177
        hints.push(
178
            response.headers()[header::WWW_AUTHENTICATE]
179
                .to_str()
180
                .unwrap()
181
                .to_owned(),
182
        );
183
    }
184
    assert!(hints[2].contains("Be honest."));
185
}
186
187
#[tokio::test]
188
async fn the_true_ending_is_not_modified() {
189
    let (app, etag) = seeded(Floor::Clinic);
190
    let early = seeded(Floor::Bedroom).0;
191
    let response = send(&early, "GET", "/runs/cafebabe/diagnosis", &[], "").await;
192
    assert_eq!(response.status(), StatusCode::TOO_EARLY);
193
194
    let response = send(
195
        &app,
196
        "GET",
197
        "/runs/cafebabe/diagnosis",
198
        &[
199
            ("authorization", "Bearer honesty"),
200
            ("if-none-match", &etag),
201
        ],
202
        "",
203
    )
204
    .await;
205
    assert_eq!(response.status(), StatusCode::NOT_MODIFIED);
206
    assert_eq!(response.headers()["x-diagnosis"], "normal");
207
}
208
209
#[tokio::test]
210
async fn mom_accepts_a_known_excuse() {
211
    let state = Arc::new(AppState::default());
212
    let mut run = Run::new("cafebabe".into(), 69, 0);
213
    run.mom_present = true;
214
    state.insert(run);
215
    let app = router(state);
216
    let response = send(&app, "GET", "/runs/cafebabe", &[], "").await;
217
    assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
218
    assert_eq!(response.headers()["via"], "1.1 mom");
219
    let response = send(
220
        &app,
221
        "GET",
222
        "/runs/cafebabe",
223
        &[("authorization", "Excuse \"homework\"")],
224
        "",
225
    )
226
    .await;
227
    assert_eq!(response.status(), StatusCode::OK);
228
}
229
230
fn seeded_with(floor: Floor, setup: impl FnOnce(&mut Run)) -> Router {
231
    let state = Arc::new(AppState::default());
232
    let mut run = Run::new("cafebabe".into(), 69, 0);
233
    run.floor = floor;
234
    setup(&mut run);
235
    state.insert(run);
236
    router(state)
237
}
238
239
#[tokio::test]
240
async fn floors_end_when_they_end() {
241
    let app = seeded_with(Floor::Bedroom, |_| {});
242
    let early = send(&app, "POST", "/runs/cafebabe/floor", &[], "").await;
243
    assert_eq!(early.status(), StatusCode::TOO_EARLY);
244
    let app = seeded_with(Floor::Bedroom, |run| run.floor_turns = 6);
245
    let next = send(&app, "POST", "/runs/cafebabe/floor", &[], "").await;
246
    assert_eq!(next.status(), StatusCode::SEE_OTHER);
247
    assert_eq!(next.headers()["x-floor"], "2");
248
}
249
250
#[tokio::test]
251
async fn pump_bro_upsells() {
252
    let closed = seeded_with(Floor::Bedroom, |_| {});
253
    let response = send(&closed, "GET", "/runs/cafebabe/shop", &[], "").await;
254
    assert_eq!(response.status(), StatusCode::NOT_FOUND);
255
256
    let app = seeded_with(Floor::LockerRoom, |_| {});
257
    let pump = send(&app, "POST", "/runs/cafebabe/shop/pump", &[], "").await;
258
    assert_eq!(pump.status(), StatusCode::UPGRADE_REQUIRED);
259
    assert_eq!(pump.headers()[header::UPGRADE], "pump-pro/2.0");
260
    let pro = send(&app, "POST", "/runs/cafebabe/shop/pump-pro", &[], "").await;
261
    assert_eq!(pro.status(), StatusCode::CREATED);
262
    let broke = send(&app, "POST", "/runs/cafebabe/shop/extender", &[], "").await;
263
    assert_eq!(broke.status(), StatusCode::PAYMENT_REQUIRED);
264
}
265
266
#[tokio::test]
267
async fn combos_are_multi_status_xml() {
268
    let app = seeded_with(Floor::Bedroom, |run| run.foreskin = Some(1.0));
269
    send(&app, "JELQ", "/runs/cafebabe?technique=warmup", &[], "").await;
270
    let response = send(
271
        &app,
272
        "JELQ",
273
        "/runs/cafebabe?technique=wet,stretch",
274
        &[("content-type", "application/baby-oil")],
275
        "",
276
    )
277
    .await;
278
    assert_eq!(response.status(), StatusCode::MULTI_STATUS);
279
    let body = to_bytes(response.into_body(), usize::MAX).await.unwrap();
280
    assert!(String::from_utf8_lossy(&body).contains("<D:multistatus"));
281
}
282
283
#[tokio::test]
284
async fn the_elder_is_stale() {
285
    let app = seeded_with(Floor::Forum, |run| run.length = 5.0);
286
    let unverified = send(&app, "GET", "/runs/cafebabe/elder", &[], "").await;
287
    assert_eq!(
288
        unverified.status(),
289
        StatusCode::PROXY_AUTHENTICATION_REQUIRED
290
    );
291
    let lie = send(
292
        &app,
293
        "GET",
294
        "/runs/cafebabe/elder",
295
        &[("proxy-authorization", "Ruler 9")],
296
        "",
297
    )
298
    .await;
299
    assert_eq!(lie.status(), StatusCode::BAD_REQUEST);
300
    let csv = send(
301
        &app,
302
        "GET",
303
        "/runs/cafebabe/elder",
304
        &[("proxy-authorization", "Ruler 5.0")],
305
        "",
306
    )
307
    .await;
308
    assert_eq!(csv.status(), StatusCode::OK);
309
    let stale = send(
310
        &app,
311
        "GET",
312
        "/runs/cafebabe/elder",
313
        &[("if-modified-since", "Mon, 15 Jun 2009 00:00:00 GMT")],
314
        "",
315
    )
316
    .await;
317
    assert_eq!(stale.status(), StatusCode::NOT_MODIFIED);
318
    let exit = send(&app, "POST", "/runs/cafebabe/floor", &[], "").await;
319
    assert_eq!(exit.status(), StatusCode::SEE_OTHER);
320
}
321
322
#[tokio::test]
323
async fn the_hotel_has_a_captive_portal() {
324
    let app = seeded_with(Floor::Hotel, |_| {});
325
    let blocked = send(&app, "GET", "/runs/cafebabe", &[], "").await;
326
    assert_eq!(
327
        blocked.status(),
328
        StatusCode::NETWORK_AUTHENTICATION_REQUIRED
329
    );
330
    let accepted = send(&app, "POST", "/runs/cafebabe/portal", &[], "").await;
331
    assert_eq!(accepted.status(), StatusCode::SEE_OTHER);
332
    let online = send(&app, "GET", "/runs/cafebabe", &[], "").await;
333
    assert_eq!(online.status(), StatusCode::OK);
334
}
335
336
#[tokio::test]
337
async fn mom_remembers_excuses_by_thanksgiving() {
338
    let app = seeded_with(Floor::Grandmas, |run| {
339
        run.mom_present = true;
340
        run.nonce = "abc".into();
341
        run.heard.push("homework".into());
342
    });
343
    let basic = send(
344
        &app,
345
        "GET",
346
        "/runs/cafebabe",
347
        &[("authorization", "Excuse \"changing\"")],
348
        "",
349
    )
350
    .await;
351
    assert_eq!(basic.status(), StatusCode::UNAUTHORIZED);
352
    assert!(
353
        basic.headers()[header::WWW_AUTHENTICATE]
354
            .to_str()
355
            .unwrap()
356
            .starts_with("Digest")
357
    );
358
    let replay = send(
359
        &app,
360
        "GET",
361
        "/runs/cafebabe",
362
        &[(
363
            "authorization",
364
            "Digest username=\"homework\", nonce=\"abc\"",
365
        )],
366
        "",
367
    )
368
    .await;
369
    assert_eq!(
370
        json(replay).await["type"],
371
        "https://jelq.dev/problems/excuse-replayed"
372
    );
373
    let fresh = send(
374
        &app,
375
        "GET",
376
        "/runs/cafebabe",
377
        &[(
378
            "authorization",
379
            "Digest username=\"praying\", nonce=\"abc\"",
380
        )],
381
        "",
382
    )
383
    .await;
384
    assert_eq!(fresh.status(), StatusCode::OK);
385
}
386
387
#[tokio::test]
388
async fn length_supports_inch_ranges() {
389
    let app = seeded_with(Floor::Bedroom, |_| {});
390
    let response = send(
391
        &app,
392
        "GET",
393
        "/runs/cafebabe/length",
394
        &[("range", "inches=0-1")],
395
        "",
396
    )
397
    .await;
398
    assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT);
399
    assert!(
400
        response.headers()[header::CONTENT_RANGE]
401
            .to_str()
402
            .unwrap()
403
            .starts_with("inches 0.00-1.00/")
404
    );
405
    let body = to_bytes(response.into_body(), usize::MAX).await.unwrap();
406
    assert_eq!(body.len(), 25);
407
    let absurd = send(
408
        &app,
409
        "GET",
410
        "/runs/cafebabe/length",
411
        &[("range", "inches=40-50")],
412
        "",
413
    )
414
    .await;
415
    assert_eq!(absurd.status(), StatusCode::RANGE_NOT_SATISFIABLE);
416
}
417
418
#[tokio::test]
419
async fn http_1_0_gets_the_pdf_treatment() {
420
    let request = Request::builder()
421
        .uri("/graveyard")
422
        .version(axum::http::Version::HTTP_10)
423
        .body(Body::empty())
424
        .unwrap();
425
    let response = jelqd::app().oneshot(request).await.unwrap();
426
    assert_eq!(response.status(), StatusCode::HTTP_VERSION_NOT_SUPPORTED);
427
}
428
429
#[tokio::test]
430
async fn the_displayed_length_is_accepted_as_honest() {
431
    let app = seeded_with(Floor::Forum, |run| run.length = 5.2496);
432
    let response = send(
433
        &app,
434
        "GET",
435
        "/runs/cafebabe/elder",
436
        &[("proxy-authorization", "Ruler 5.25")],
437
        "",
438
    )
439
    .await;
440
    assert_eq!(response.status(), StatusCode::OK);
441
}