diff --git a/Cargo.lock b/Cargo.lock index d19a11f1..d7412574 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -70,12 +70,12 @@ dependencies = [ [[package]] name = "actix-macros" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "465a6172cf69b960917811022d8f29bc0b7fa1398bc4f78b3c466673db1213b6" +checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" dependencies = [ "quote", - "syn 1.0.109", + "syn 2.0.26", ] [[package]] @@ -379,6 +379,16 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" +[[package]] +name = "assert-json-diff" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "assert_fs" version = "1.0.13" @@ -1110,9 +1120,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.3.16" +version = "4.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74bb1b4028935821b2d6b439bba2e970bdcf740832732437ead910c632e30d7d" +checksum = "5b0827b011f6f8ab38590295339817b0d26f344aa4932c3ced71b45b0c54b4a9" dependencies = [ "clap_builder", "clap_derive", @@ -1121,9 +1131,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.3.16" +version = "4.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ae467cbb0111869b765e13882a1dbbd6cb52f58203d8b80c44f667d4dd19843" +checksum = "9441b403be87be858db6a23edb493e7f694761acdc3343d5a0fcaafd304cbc9e" dependencies = [ "anstream", "anstyle", @@ -1416,6 +1426,25 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" +[[package]] +name = "deadpool" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "421fe0f90f2ab22016f32a9881be5134fdd71c65298917084b0c7477cbc3856e" +dependencies = [ + "async-trait", + "deadpool-runtime", + "num_cpus", + "retain_mut", + "tokio", +] + +[[package]] +name = "deadpool-runtime" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaa37046cc0f6c3cc6090fbdbf73ef0b8ef4cfcc37f6befc0020f63e8cf121e1" + [[package]] name = "debugid" version = "0.8.0" @@ -2253,6 +2282,27 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bfe8eed0a9285ef776bb792479ea3834e8b94e13d615c2f66d03dd50a435a29" +[[package]] +name = "http-types" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9b187a72d63adbfba487f48095306ac823049cb504ee195541e91c7775f5ad" +dependencies = [ + "anyhow", + "async-channel", + "base64 0.13.1", + "futures-lite", + "http", + "infer", + "pin-project-lite", + "rand 0.7.3", + "serde", + "serde_json", + "serde_qs", + "serde_urlencoded", + "url", +] + [[package]] name = "httparse" version = "1.8.0" @@ -2446,6 +2496,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "infer" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64e9829a50b42bb782c1df523f78d332fe371b10c661e78b7a3c34b0198e9fac" + [[package]] name = "inferno" version = "0.11.15" @@ -4313,9 +4369,9 @@ dependencies = [ [[package]] name = "scopeguard" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "scrypt" @@ -4385,9 +4441,9 @@ checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" [[package]] name = "serde" -version = "1.0.171" +version = "1.0.173" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30e27d1e4fd7659406c492fd6cfaf2066ba8773de45ca75e855590f856dc34a9" +checksum = "e91f70896d6720bc714a4a57d22fc91f1db634680e65c8efe13323f1fa38d53f" dependencies = [ "serde_derive", ] @@ -4413,9 +4469,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.171" +version = "1.0.173" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389894603bd18c46fa56231694f8d827779c0951a667087194cf9de94ed24682" +checksum = "a6250dde8342e0232232be9ca3db7aa40aceb5a3e5dd9bddbc00d99a007cde49" dependencies = [ "proc-macro2", "quote", @@ -4424,9 +4480,9 @@ dependencies = [ [[package]] name = "serde_html_form" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7d9b8af723e4199801a643c622daa7aae8cf4a772dc2b3efcb3a95add6cb91a" +checksum = "cde65b75f2603066b78d6fa239b2c07b43e06ead09435f60554d3912962b4a3c" dependencies = [ "form_urlencoded", "indexmap 2.0.0", @@ -4456,6 +4512,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_qs" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7715380eec75f029a4ef7de39a9200e0a63823176b759d055b613f5a87df6a6" +dependencies = [ + "percent-encoding", + "serde", + "thiserror", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -4690,7 +4757,7 @@ dependencies = [ "axum-server", "base64 0.21.2", "bytes", - "clap 4.3.16", + "clap 4.3.17", "futures 0.3.28", "futures-util", "glob", @@ -4810,6 +4877,7 @@ dependencies = [ "uuid", "wasm-bindgen-futures", "wasmtimer", + "wiremock", "ws_stream_wasm", ] @@ -5595,6 +5663,7 @@ dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] [[package]] @@ -5946,6 +6015,28 @@ dependencies = [ "winapi", ] +[[package]] +name = "wiremock" +version = "0.5.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f71803d3a1c80377a06221e0530be02035d5b3e854af56c6ece7ac20ac441d" +dependencies = [ + "assert-json-diff", + "async-trait", + "base64 0.21.2", + "deadpool", + "futures 0.3.28", + "futures-timer", + "http-types", + "hyper", + "log", + "once_cell", + "regex", + "serde", + "serde_json", + "tokio", +] + [[package]] name = "ws_stream_wasm" version = "0.7.4" @@ -5976,9 +6067,9 @@ dependencies = [ [[package]] name = "xml-rs" -version = "0.8.15" +version = "0.8.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a56c84a8ccd4258aed21c92f70c0f6dea75356b6892ae27c24139da456f9336" +checksum = "47430998a7b5d499ccee752b41567bc3afc57e1327dc855b1a2aa44ce29b5fa1" [[package]] name = "yasna" @@ -5997,18 +6088,18 @@ checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" [[package]] name = "zstd" -version = "0.12.3+zstd.1.5.2" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76eea132fb024e0e13fd9c2f5d5d595d8a967aa72382ac2f9d39fcc95afd0806" +checksum = "1a27595e173641171fc74a1232b7b1c7a7cb6e18222c11e9dfb9888fa424c53c" dependencies = [ "zstd-safe", ] [[package]] name = "zstd-safe" -version = "6.0.5+zstd.1.5.4" +version = "6.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56d9e60b4b1758206c238a10165fbcae3ca37b01744e394c463463f6529d23b" +checksum = "ee98ffd0b48ee95e6c5168188e44a54550b1564d9d530ee21d5f0eaed1069581" dependencies = [ "libc", "zstd-sys", diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 1535e1a2..17a30d5b 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -124,6 +124,7 @@ temp-dir = "0.1.11" time = { version = "0.3.23", features = ["serde"] } tokio = { version = "1.29.1", features = ["macros", "sync", "rt-multi-thread"] } tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } +wiremock = "0.5.19" [target.'cfg(target_arch = "wasm32")'.dependencies] pharos = "0.5.3" diff --git a/lib/src/fnc/script/fetch/func.rs b/lib/src/fnc/script/fetch/func.rs index 40dc9fac..e8773b07 100644 --- a/lib/src/fnc/script/fetch/func.rs +++ b/lib/src/fnc/script/fetch/func.rs @@ -118,3 +118,146 @@ pub async fn fetch<'js>( }; Ok(response) } + +#[cfg(test)] +mod test { + use crate::fnc::script::fetch::test::create_test_context; + + #[tokio::test] + async fn test_fetch_get() { + use js::{promise::Promise, CatchResultExt}; + use wiremock::{ + matchers::{header, method, path}, + Mock, MockServer, ResponseTemplate, + }; + + let server = MockServer::start().await; + + Mock::given(method("GET")) + .and(path("/hello")) + .and(header("some-header", "some-value")) + .respond_with(ResponseTemplate::new(200).set_body_string("some body once told me")) + .expect(1) + .mount(&server) + .await; + + let server_ref = &server; + + create_test_context!(ctx => { + ctx.globals().set("SERVER_URL",server_ref.uri()).unwrap(); + + ctx.eval::<Promise<()>,_>(r#" + (async () => { + let res = await fetch(SERVER_URL + '/hello',{ + headers: { + "some-header": "some-value", + } + }); + assert.seq(res.status,200); + let body = await res.text(); + assert.seq(body,'some body once told me'); + })() + "#).catch(&ctx).unwrap().await.catch(&ctx).unwrap() + }) + .await; + + server.verify().await; + } + + #[tokio::test] + async fn test_fetch_put() { + use js::{promise::Promise, CatchResultExt}; + use wiremock::{ + matchers::{body_string, header, method, path}, + Mock, MockServer, ResponseTemplate, + }; + + let server = MockServer::start().await; + + Mock::given(method("PUT")) + .and(path("/hello")) + .and(header("some-header", "some-value")) + .and(body_string("some text")) + .respond_with(ResponseTemplate::new(201).set_body_string("some body once told me")) + .expect(1) + .mount(&server) + .await; + + let server_ref = &server; + + create_test_context!(ctx => { + ctx.globals().set("SERVER_URL",server_ref.uri()).unwrap(); + + ctx.eval::<Promise<()>,_>(r#" + (async () => { + let res = await fetch(SERVER_URL + '/hello',{ + method: "PuT", + headers: { + "some-header": "some-value", + }, + body: "some text", + }); + assert.seq(res.status,201); + assert(res.ok); + let body = await res.text(); + assert.seq(body,'some body once told me'); + })() + "#).catch(&ctx).unwrap().await.catch(&ctx).unwrap() + }) + .await; + + server.verify().await; + } + + #[tokio::test] + async fn test_fetch_error() { + use js::{promise::Promise, CatchResultExt}; + use wiremock::{ + matchers::{body_string, header, method, path}, + Mock, MockServer, ResponseTemplate, + }; + + let server = MockServer::start().await; + + Mock::given(method("PROPPATCH")) + .and(path("/hello")) + .and(header("some-header", "some-value")) + .and(body_string("some text")) + .respond_with(ResponseTemplate::new(500).set_body_json(serde_json::json!({ + "foo": "bar", + "baz": 2, + }))) + .expect(1) + .mount(&server) + .await; + + let server_ref = &server; + + create_test_context!(ctx => { + ctx.globals().set("SERVER_URL",server_ref.uri()).unwrap(); + + ctx.eval::<Promise<()>,_>(r#" + (async () => { + let req = new Request(SERVER_URL + '/hello',{ + method: "PROPPATCH", + headers: { + "some-header": "some-value", + }, + body: "some text", + }) + let res = await fetch(req); + assert.seq(res.status,500); + assert(!res.ok); + let body = await res.json(); + assert(body.foo !== undefined, "body.foo not defined"); + assert(body.baz !== undefined, "body.foo not defined"); + assert.seq(body.foo, "bar"); + assert.seq(body.baz, 2); + })() + "#).catch(&ctx).unwrap().await.catch(&ctx).unwrap() + }) + .await; + + server.verify().await; + } +} diff --git a/lib/src/fnc/script/fetch/mod.rs b/lib/src/fnc/script/fetch/mod.rs index 40f831e9..ff1f3ee1 100644 --- a/lib/src/fnc/script/fetch/mod.rs +++ b/lib/src/fnc/script/fetch/mod.rs @@ -57,21 +57,19 @@ mod test { crate::fnc::script::fetch::register(&$ctx).unwrap(); $ctx.eval::<(),_>(r" - globalThis.assert = (...arg) => { - arg.forEach(x => { - if (!x) { - throw new Error('assertion failed') - } - }) - }; - assert.eq = (a,b) => { - if(a != b){ - throw new Error(`assertion failed, '${a}' != '${b}'`) + globalThis.assert = (arg, text) => { + if (!arg) { + throw new Error('assertion failed ' + (text ? text : '')) } }; - assert.seq = (a,b) => { + assert.eq = (a,b, text) => { + if(a != b){ + throw new Error(`assertion failed, '${a}' != '${b}'` + (text ? text : '')) + } + }; + assert.seq = (a,b, text) => { if(!(a === b)){ - throw new Error(`assertion failed, '${a}' !== '${b}'`) + throw new Error(`assertion failed, '${a}' !== '${b}'` +( text ? text : '')) } }; assert.mustThrow = (cb) => { diff --git a/lib/tests/function.rs b/lib/tests/function.rs index 250261a0..2d90f232 100644 --- a/lib/tests/function.rs +++ b/lib/tests/function.rs @@ -12,6 +12,7 @@ async fn test_queries(sql: &str, desired_responses: &[&str]) -> Result<(), Error for (i, r) in response.into_iter().map(|r| r.result).enumerate() { let v = r?; if let Some(desired_response) = desired_responses.get(i) { + dbg!(desired_response); let desired_value = Value::parse(*desired_response); // If both values are NaN, they are equal from a test PoV if !desired_value.is_nan() || !v.is_nan() { @@ -5178,3 +5179,226 @@ async fn function_vector_distance_chebyshev() -> Result<(), Error> { ]).await?; Ok(()) } + +#[cfg(feature = "http")] +#[tokio::test] +pub async fn function_http_head() -> Result<(), Error> { + use wiremock::{ + matchers::{header, method, path}, + Mock, ResponseTemplate, + }; + + let server = wiremock::MockServer::start().await; + Mock::given(method("HEAD")) + .and(path("/some/path")) + .and(header("user-agent", "SurrealDB")) + .respond_with(ResponseTemplate::new(200)) + .expect(1) + .mount(&server) + .await; + + test_queries(&format!("RETURN http::head('{}/some/path')", server.uri()), &["NONE"]).await?; + + server.verify().await; + + Ok(()) +} + +#[cfg(feature = "http")] +#[tokio::test] +pub async fn function_http_get() -> Result<(), Error> { + use wiremock::{ + matchers::{header, method, path}, + Mock, ResponseTemplate, + }; + + let server = wiremock::MockServer::start().await; + Mock::given(method("GET")) + .and(path("/some/path")) + .and(header("user-agent", "SurrealDB")) + .and(header("a-test-header", "with-a-test-value")) + .respond_with(ResponseTemplate::new(200).set_body_string("some text result")) + .expect(1) + .mount(&server) + .await; + + let query = format!( + r#"RETURN http::get("{}/some/path",{{ 'a-test-header': 'with-a-test-value'}})"#, + server.uri() + ); + test_queries(&query, &["'some text result'"]).await?; + + server.verify().await; + + Ok(()) +} + +#[cfg(feature = "http")] +#[tokio::test] +pub async fn function_http_put() -> Result<(), Error> { + use wiremock::{ + matchers::{header, method, path}, + Mock, ResponseTemplate, + }; + + let server = wiremock::MockServer::start().await; + Mock::given(method("PUT")) + .and(path("/some/path")) + .and(header("user-agent", "SurrealDB")) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "some-response": "some-value" + }))) + .expect(1) + .mount(&server) + .await; + + let query = + format!(r#"RETURN http::put("{}/some/path",{{ 'some-key': 'some-value' }})"#, server.uri()); + test_queries(&query, &[r#"{ "some-response": 'some-value' }"#]).await?; + + server.verify().await; + + Ok(()) +} + +#[cfg(feature = "http")] +#[tokio::test] +pub async fn function_http_post() -> Result<(), Error> { + use wiremock::{ + matchers::{header, method, path}, + Mock, ResponseTemplate, + }; + + let server = wiremock::MockServer::start().await; + Mock::given(method("POST")) + .and(path("/some/path")) + .and(header("user-agent", "SurrealDB")) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "some-response": "some-value" + }))) + .expect(1) + .mount(&server) + .await; + + let query = format!( + r#"RETURN http::post("{}/some/path",{{ 'some-key': 'some-value' }})"#, + server.uri() + ); + test_queries(&query, &[r#"{ "some-response": 'some-value' }"#]).await?; + + server.verify().await; + + Ok(()) +} + +#[cfg(feature = "http")] +#[tokio::test] +pub async fn function_http_patch() -> Result<(), Error> { + use wiremock::{ + matchers::{header, method, path}, + Mock, ResponseTemplate, + }; + + let server = wiremock::MockServer::start().await; + Mock::given(method("PATCH")) + .and(path("/some/path")) + .and(header("user-agent", "SurrealDB")) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "some-response": "some-value" + }))) + .expect(1) + .mount(&server) + .await; + + let query = format!( + r#"RETURN http::patch("{}/some/path",{{ 'some-key': 'some-value' }})"#, + server.uri() + ); + test_queries(&query, &[r#"{ "some-response": 'some-value' }"#]).await?; + + server.verify().await; + + Ok(()) +} + +#[cfg(feature = "http")] +#[tokio::test] +pub async fn function_http_delete() -> Result<(), Error> { + use wiremock::{ + matchers::{header, method, path}, + Mock, ResponseTemplate, + }; + + let server = wiremock::MockServer::start().await; + Mock::given(method("DELETE")) + .and(path("/some/path")) + .and(header("user-agent", "SurrealDB")) + .and(header("a-test-header", "with-a-test-value")) + .respond_with(ResponseTemplate::new(200).set_body_string("some text result")) + .expect(1) + .mount(&server) + .await; + + let query = format!( + r#"RETURN http::delete("{}/some/path",{{ 'a-test-header': 'with-a-test-value'}})"#, + server.uri() + ); + test_queries(&query, &["'some text result'"]).await?; + + server.verify().await; + + Ok(()) +} + +#[cfg(feature = "http")] +#[tokio::test] +pub async fn function_http_error() -> Result<(), Error> { + use wiremock::{ + matchers::{header, method, path}, + Mock, ResponseTemplate, + }; + + let server = wiremock::MockServer::start().await; + Mock::given(method("GET")) + .and(path("/some/path")) + .and(header("user-agent", "SurrealDB")) + .and(header("a-test-header", "with-a-test-value")) + .respond_with(ResponseTemplate::new(500).set_body_string("some text result")) + .expect(1) + .mount(&server) + .await; + + let query = format!( + r#"RETURN http::get("{}/some/path",{{ 'a-test-header': 'with-a-test-value'}})"#, + server.uri() + ); + + let res = test_queries(&query, &["NONE"]).await; + match res { + Err(Error::Http(text)) => { + assert_eq!(text, "Internal Server Error"); + } + e => panic!("query didn't return correct response: {:?}", e), + } + + server.verify().await; + + Ok(()) +} + +#[cfg(not(feature = "http"))] +#[tokio::test] +pub async fn function_http_disabled() -> Result<(), Error> { + let res = test_queries("RETURN http::head({})", &["NONE"]).await; + assert!(matches!(res, Err(Error::HttpDisabled))); + let res = test_queries("RETURN http::put({})", &["NONE"]).await; + assert!(matches!(res, Err(Error::HttpDisabled))); + let res = test_queries("RETURN http::post({})", &["NONE"]).await; + assert!(matches!(res, Err(Error::HttpDisabled))); + let res = test_queries("RETURN http::patch({})", &["NONE"]).await; + assert!(matches!(res, Err(Error::HttpDisabled))); + let res = test_queries("RETURN http::delete({})", &["NONE"]).await; + assert!(matches!(res, Err(Error::HttpDisabled))); + + Ok(()) +}