1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
#![cfg_attr(test, allow(clippy::expect_used, clippy::unwrap_used))]

use std::path::Path;
use std::process::Child;
use std::process::Command;
use std::time::Duration;
use std::time::Instant;

use serde_json::Value;
use serde_json::json;

/// A running fixture that dies with the test.
struct Fixture {
    child: Child,
    url: String,
    data: Value,
    agent: ureq::Agent,
    _dir: tempfile::TempDir,
}

impl Drop for Fixture {
    fn drop(&mut self) {
        let _ = self.child.kill();
        let _ = self.child.wait();
    }
}

impl Fixture {
    fn start() -> Self {
        let dir = tempfile::tempdir().expect("tempdir");
        let handshake = dir.path().join("handshake.json");
        let child = Command::new(env!("CARGO_BIN_EXE_parallax-fixture"))
            .env("PARALLAX_HANDSHAKE", &handshake)
            .spawn()
            .expect("spawn fixture");
        let body = wait_for(&handshake);
        let url = body["url"].as_str().expect("handshake url").to_owned();
        let agent = ureq::Agent::config_builder()
            .http_status_as_error(false)
            .build()
            .into();
        Fixture {
            child,
            url,
            data: body["data"].clone(),
            agent,
            _dir: dir,
        }
    }

    fn get(&self, path: &str) -> (u16, String) {
        let mut response = self
            .agent
            .get(format!("{}{path}", self.url))
            .call()
            .expect("get");
        let body = response.body_mut().read_to_string().expect("body");
        (response.status().as_u16(), body)
    }

    fn post_json(&self, path: &str, body: Value) -> (u16, Value) {
        let mut response = self
            .agent
            .post(format!("{}{path}", self.url))
            .send_json(body)
            .expect("post");
        let body = response.body_mut().read_json().expect("json body");
        (response.status().as_u16(), body)
    }
}

fn wait_for(handshake: &Path) -> Value {
    let deadline = Instant::now() + Duration::from_secs(10);
    loop {
        if let Ok(text) = fs_err::read_to_string(handshake) {
            return serde_json::from_str(&text).expect("handshake is complete JSON");
        }
        assert!(Instant::now() < deadline, "no handshake after 10s");
        std::thread::sleep(Duration::from_millis(20));
    }
}

#[test]
fn handshake_serves_the_site() {
    let app = Fixture::start();
    assert_eq!(app.data, json!({"featured_item": 2}));
    let (status, body) = app.get("/");
    assert_eq!(status, 200);
    assert!(body.contains(r#"class="dashboard""#));
    assert!(body.contains("Water the ferns"));
    let (status, body) = app.get("/items/2.html");
    assert_eq!(status, 200);
    assert!(body.contains(r#"class="item-detail""#));
}

#[test]
fn fixture_route_round_trips() {
    let app = Fixture::start();
    let (status, item) = app.post_json(
        "/__fixtures/items",
        json!({"title": "Added by the flow", "tag": "parallax"}),
    );
    assert_eq!(status, 201);
    assert_eq!(
        item,
        json!({"id": 6, "title": "Added by the flow", "tag": "parallax", "notes": "", "done": false}),
    );
    let (status, body) = app.post_json("/__fixtures/items", json!({"title": "  "}));
    assert_eq!(status, 422);
    assert_eq!(body["error"], "title must not be empty");
}