|
| 1 | +use std::collections::HashMap; |
| 2 | + |
| 3 | +use anyhow::Context as _; |
| 4 | + |
| 5 | +fn main() { |
| 6 | + let dir = std::fs::read_dir("/Users/rylev/Code/fermyon/conformance-test/tests").unwrap(); |
| 7 | + for entry in dir { |
| 8 | + let entry = entry.unwrap(); |
| 9 | + let spin_binary = "/Users/rylev/.local/bin/spin".into(); |
| 10 | + let test_config = std::fs::read_to_string(entry.path().join("test.json5")).unwrap(); |
| 11 | + let test_config: TestConfig = json5::from_str(&test_config).unwrap(); |
| 12 | + let config = testing_framework::TestEnvironmentConfig::spin( |
| 13 | + spin_binary, |
| 14 | + ["-f".into(), entry.path().to_str().unwrap().into()], |
| 15 | + move |e| { |
| 16 | + e.copy_into(entry.path().join("spin.toml"), "spin.toml") |
| 17 | + .context("failed to copy spin.toml")?; |
| 18 | + let mut cmd = std::process::Command::new("spin"); |
| 19 | + cmd.env("CARGO_TARGET_DIR", entry.path().join("target")); |
| 20 | + cmd.args(["build", "-f", entry.path().to_str().unwrap()]); |
| 21 | + e.run_in(&mut cmd)?; |
| 22 | + Ok(()) |
| 23 | + }, |
| 24 | + testing_framework::ServicesConfig::none(), |
| 25 | + testing_framework::runtimes::SpinAppType::Http, |
| 26 | + ); |
| 27 | + let mut env = testing_framework::TestEnvironment::up(config, |_| Ok(())).unwrap(); |
| 28 | + let spin = env.runtime_mut(); |
| 29 | + for invocation in test_config.invocations { |
| 30 | + let headers = invocation |
| 31 | + .request |
| 32 | + .headers |
| 33 | + .iter() |
| 34 | + .map(|h| (h.name.as_str(), h.value.as_str())) |
| 35 | + .collect::<Vec<_>>(); |
| 36 | + let request = testing_framework::http::Request::full( |
| 37 | + invocation.request.method.into(), |
| 38 | + &invocation.request.path, |
| 39 | + &headers, |
| 40 | + invocation.request.body, |
| 41 | + ); |
| 42 | + let response = spin.make_http_request(request).unwrap(); |
| 43 | + let stderr = spin.stderr(); |
| 44 | + let body = String::from_utf8(response.body()) |
| 45 | + .unwrap_or_else(|_| String::from("invalid utf-8")); |
| 46 | + assert_eq!( |
| 47 | + response.status(), |
| 48 | + invocation.response.status, |
| 49 | + "request to Spin failed\nstderr:\n{stderr}\nbody:\n{body}", |
| 50 | + ); |
| 51 | + |
| 52 | + let mut actual_headers = response |
| 53 | + .headers() |
| 54 | + .iter() |
| 55 | + .map(|(k, v)| (k.to_lowercase(), v.to_lowercase())) |
| 56 | + .collect::<HashMap<_, _>>(); |
| 57 | + for expected_header in invocation.response.headers { |
| 58 | + let expected_name = expected_header.name.to_lowercase(); |
| 59 | + let expected_value = expected_header.value.map(|v| v.to_lowercase()); |
| 60 | + let actual_value = actual_headers.remove(&expected_name); |
| 61 | + let Some(actual_value) = actual_value.as_deref() else { |
| 62 | + if expected_header.optional { |
| 63 | + continue; |
| 64 | + } else { |
| 65 | + panic!( |
| 66 | + "expected header {name} not found in response", |
| 67 | + name = expected_header.name |
| 68 | + ) |
| 69 | + } |
| 70 | + }; |
| 71 | + if let Some(expected_value) = expected_value { |
| 72 | + assert_eq!(actual_value, expected_value); |
| 73 | + } |
| 74 | + } |
| 75 | + if !actual_headers.is_empty() { |
| 76 | + panic!("unexpected headers: {actual_headers:?}"); |
| 77 | + } |
| 78 | + |
| 79 | + if let Some(expected_body) = invocation.response.body { |
| 80 | + assert_eq!(body, expected_body); |
| 81 | + } |
| 82 | + } |
| 83 | + } |
| 84 | +} |
| 85 | + |
| 86 | +#[derive(Debug, serde::Deserialize)] |
| 87 | +struct TestConfig { |
| 88 | + invocations: Vec<Invocation>, |
| 89 | +} |
| 90 | + |
| 91 | +#[derive(Debug, serde::Deserialize)] |
| 92 | +struct Invocation { |
| 93 | + request: Request, |
| 94 | + response: Response, |
| 95 | +} |
| 96 | + |
| 97 | +#[derive(Debug, serde::Deserialize)] |
| 98 | +struct Request { |
| 99 | + #[serde(default)] |
| 100 | + method: Method, |
| 101 | + path: String, |
| 102 | + #[serde(default)] |
| 103 | + headers: Vec<RequestHeader>, |
| 104 | + #[serde(default)] |
| 105 | + body: Option<String>, |
| 106 | +} |
| 107 | + |
| 108 | +#[derive(Debug, serde::Deserialize)] |
| 109 | +struct Response { |
| 110 | + #[serde(default = "default_status")] |
| 111 | + status: u16, |
| 112 | + headers: Vec<ResponseHeader>, |
| 113 | + body: Option<String>, |
| 114 | +} |
| 115 | +#[derive(Debug, serde::Deserialize)] |
| 116 | +struct RequestHeader { |
| 117 | + name: String, |
| 118 | + value: String, |
| 119 | +} |
| 120 | + |
| 121 | +#[derive(Debug, serde::Deserialize)] |
| 122 | +struct ResponseHeader { |
| 123 | + name: String, |
| 124 | + value: Option<String>, |
| 125 | + #[serde(default)] |
| 126 | + optional: bool, |
| 127 | +} |
| 128 | + |
| 129 | +#[derive(Debug, serde::Deserialize, Default)] |
| 130 | +enum Method { |
| 131 | + #[default] |
| 132 | + GET, |
| 133 | + POST, |
| 134 | +} |
| 135 | + |
| 136 | +impl From<Method> for testing_framework::http::Method { |
| 137 | + fn from(method: Method) -> Self { |
| 138 | + match method { |
| 139 | + Method::GET => testing_framework::http::Method::GET, |
| 140 | + Method::POST => testing_framework::http::Method::POST, |
| 141 | + } |
| 142 | + } |
| 143 | +} |
| 144 | + |
| 145 | +fn default_status() -> u16 { |
| 146 | + 200 |
| 147 | +} |
0 commit comments