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
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
//! Support for mock HTTP servers that verify pacts.

use pact_matching::models::*;
use pact_mock_server::matching::MatchResult;
use pact_mock_server::*;
use std::{
    env,
    fmt::Write as FmtWrite,
    io::{self, prelude::*},
    thread,
};
use url::Url;
use pact_mock_server::mock_server::MockServerConfig;
use std::sync::{Mutex, Arc};

/// This trait is implemented by types which allow us to start a mock server.
pub trait StartMockServer {
    /// Start a mock server running in a background thread.
    fn start_mock_server(&self) -> ValidatingMockServer;
}

impl StartMockServer for RequestResponsePact {
    fn start_mock_server(&self) -> ValidatingMockServer {
        ValidatingMockServer::start(self.clone())
    }
}

/// A mock HTTP server that handles the requests described in a `Pact`, intended
/// for use in tests, and validates that the requests made to that server are
/// correct.
///
/// Because this is intended for use in tests, it will panic if something goes
/// wrong.
pub struct ValidatingMockServer {
    // A description of our mock server, for use in error messages.
    description: String,
    // The URL of our mock server.
    url: Url,
    // The mock server instance
    mock_server: Arc<Mutex<mock_server::MockServer>>,
    // Signal received when the server thread is done executing
    done_rx: std::sync::mpsc::Receiver<()>,
}

impl ValidatingMockServer {
    /// Create a new mock server which handles requests as described in the
    /// pact, and runs in a background thread
    pub fn start(pact: RequestResponsePact) -> ValidatingMockServer {
        // Spawn new runtime in thread to prevent reactor execution context conflict
        let (mock_server, done_rx) = std::thread::spawn(move || {
            let mut runtime = tokio::runtime::Builder::new()
                .basic_scheduler()
                .enable_all()
                .build()
                .expect("new runtime");

            let (mock_server, server_future) = runtime.block_on(async move {
              mock_server::MockServer::new("".into(), pact, ([0, 0, 0, 0], 0 as u16).into(),
                MockServerConfig::default())
                .await
                .unwrap()
            });

            // Start the actual thread the runtime will run on
            let (done_tx, done_rx) = std::sync::mpsc::channel::<()>();
            let tname = format!(
                "test({})-pact-mock-server",
                thread::current().name().unwrap_or("<unknown>")
            );
            std::thread::Builder::new()
                .name(tname)
                .spawn(move || {
                    runtime.block_on(server_future);
                    let _ = done_tx.send(());
                })
                .expect("thread spawn");

            (mock_server, done_rx)
        })
        .join()
        .unwrap();

        let (description, url_str) = {
          let ms = mock_server.lock().unwrap();
          let description = format!(
            "{}/{}", ms.pact.consumer.name, ms.pact.provider.name
          );
          (description, ms.url())
        };
        ValidatingMockServer {
          description,
          url: url_str.parse().expect("invalid mock server URL"),
          mock_server,
          done_rx,
        }
    }

    /// The URL of our mock server. You can make normal HTTP requests using this
    /// as the base URL.
    pub fn url(&self) -> &Url {
        &self.url
    }

    /// Given a path string, return a URL pointing to that path on the mock
    /// server. If the `path` cannot be parsed as URL, **this function will
    /// panic**. For a non-panicking version, call `.url()` instead and build
    /// this path yourself.
    pub fn path<P: AsRef<str>>(&self, path: P) -> Url {
        // We panic here because this a _test_ library, the `?` operator is
        // useless in tests, and filling up our test code with piles of `unwrap`
        // calls is ugly.
        self.url.join(path.as_ref()).expect("could not parse URL")
    }

    /// Returns the current status of the mock server
    pub fn status(&self) -> Vec<MatchResult> {
      self.mock_server.lock().unwrap().mismatches()
    }

    /// Helper function called by our `drop` implementation. This basically exists
    /// so that it can return `Err(message)` whenever needed without making the
    /// flow control in `drop` ultra-complex.
    fn drop_helper(&mut self) -> Result<(), String> {
        // Kill the server
        let mut ms = self.mock_server.lock().unwrap();
        ms.shutdown()?;

        if ::std::thread::panicking() {
            return Ok(());
        }

        // Wait for the server thread to finish
        self.done_rx
            .recv_timeout(std::time::Duration::from_secs(3))
            .expect("mock server thread should not panic");

        // Look up any mismatches which occurred.
        let mismatches = ms.mismatches();

        if mismatches.is_empty() {
            // Success! Write out the generated pact file.
            ms.write_pact(&Some(
                env::var("PACT_OUTPUT_DIR").unwrap_or_else(|_| "target/pacts".to_owned()),
            ))
            .map_err(|err| format!("error writing pact: {}", err))?;
            Ok(())
        } else {
            // Failure. Format our errors.
            let mut msg = format!("mock server {} failed verification:\n", self.description,);
            for mismatch in mismatches {
                match mismatch {
                    MatchResult::RequestMatch(_) => {
                        unreachable!("list of mismatches contains a match");
                    }
                    MatchResult::RequestMismatch(interaction, mismatches) => {
                        let _ = writeln!(&mut msg, "- interaction {:?}:", interaction.description,);
                        for m in mismatches {
                            let _ = writeln!(&mut msg, "  - {}", m.description());
                        }
                    }
                    MatchResult::RequestNotFound(request) => {
                        let _ = writeln!(&mut msg, "- received unexpected request:");
                        let _ = writeln!(&mut msg, "{:#?}", request);
                    }
                    MatchResult::MissingRequest(interaction) => {
                        let _ = writeln!(
                            &mut msg,
                            "- interaction {:?} expected, but never occurred",
                            interaction.description,
                        );
                        let _ = writeln!(&mut msg, "{:#?}", interaction.request);
                    }
                }
            }
            Err(msg)
        }
    }
}

/// Either panic with `msg`, or if we're already in the middle of a panic,
/// just print `msg` to standard error.
fn panic_or_print_error(msg: &str) {
    if thread::panicking() {
        // The current thread is panicking, so don't try to panic again, because
        // double panics don't print useful explanations of why the test failed.
        // Instead, just print to `stderr`. Ignore any errors, because there's
        // not much we can do if we can't panic and we can't write to `stderr`.
        let _ = writeln!(io::stderr(), "{}", msg);
    } else {
        panic!("{}", msg);
    }
}

impl Drop for ValidatingMockServer {
    fn drop(&mut self) {
        let result = self.drop_helper();
        if let Err(msg) = result {
            panic_or_print_error(&msg);
        }
    }
}