From c7a25cb3777d3370cfceb3be013d1c70849d9ed9 Mon Sep 17 00:00:00 2001 From: Sandipsinh Rathod <62684960+ssddOnTop@users.noreply.github.com> Date: Wed, 1 May 2024 15:40:28 +0530 Subject: [PATCH] feat(grpc): add support for reflection (#1647) Co-authored-by: Tushar Mathur Co-authored-by: Shashi Kant Co-authored-by: meskill <8974488+meskill@users.noreply.github.com> --- Cargo.lock | 2 + Cargo.toml | 1 + examples/grpc-reflection.graphql | 36 ++ examples/lint.sh | 3 +- generated/.tailcallrc.graphql | 1 + generated/.tailcallrc.schema.json | 3 +- src/config/link.rs | 1 + src/config/reader.rs | 42 ++- src/config/reader_context.rs | 31 +- src/generator/from_proto.rs | 3 +- src/generator/generator.rs | 4 +- src/main.rs | 1 + src/proto_reader/fetch.rs | 310 ++++++++++++++++++ src/proto_reader/fixtures/descriptor_b64.txt | 1 + src/proto_reader/fixtures/response_b64.txt | 1 + src/proto_reader/mod.rs | 3 + src/proto_reader/proto/reflection.proto | 136 ++++++++ .../reader.rs} | 65 +++- .../fixtures/configs}/proto_no_pkg.graphql | 0 .../configs}/user-posts-query.graphql | 0 .../fixtures/configs}/user-posts.graphql | 0 .../fixtures}/generator/proto/greetings.proto | 0 .../generator/proto/greetings_a.proto | 0 .../generator/proto/greetings_b.proto | 0 .../generator/proto/greetings_message.proto | 0 .../fixtures}/generator/proto/news.proto | 0 .../generator/proto/news_no_service.proto | 0 .../fixtures}/generator/proto/no_pkg.proto | 0 .../fixtures}/generator/proto/person.proto | 0 .../grpc/reflection/news-list-services.bin | Bin 0 -> 75 bytes .../reflection/news-service-descriptor.bin | Bin 0 -> 1373 bytes tailcall-query-plan/Cargo.toml | 1 + tailcall-query-plan/tests/execution.rs | 2 +- tests/core/http.rs | 76 +---- tests/core/mod.rs | 2 +- tests/core/model.rs | 100 +++++- tests/core/parse.rs | 1 - tests/core/runtime.rs | 21 +- .../core/snapshots/grpc-reflection.md_0.snap | 24 ++ .../snapshots/grpc-reflection.md_client.snap | 34 ++ .../snapshots/grpc-reflection.md_merged.snap | 22 ++ tests/core/spec.rs | 21 +- tests/execution/add-field-index-list.md | 1 - tests/execution/add-field-modify.md | 1 - tests/execution/add-field-with-composition.md | 3 +- tests/execution/add-field-with-modify.md | 2 - tests/execution/add-field.md | 1 - tests/execution/apollo-tracing.md | 1 - tests/execution/batching-default.md | 2 - tests/execution/batching-disabled.md | 2 - tests/execution/batching-group-by-default.md | 2 - tests/execution/batching-group-by.md | 2 - tests/execution/batching-post.md | 2 - tests/execution/batching.md | 3 +- tests/execution/cache-control.md | 10 +- tests/execution/caching-collision.md | 101 ------ tests/execution/call-graphql-datasource.md | 9 +- tests/execution/call-mutation.md | 10 +- tests/execution/call-operator.md | 14 +- tests/execution/env-value.md | 3 - tests/execution/experimental-headers.md | 1 - .../graphql-dataloader-batch-keys.md | 6 +- .../graphql-dataloader-batch-request.md | 7 +- .../graphql-dataloader-no-batch-request.md | 5 +- tests/execution/graphql-datasource-errors.md | 4 +- .../execution/graphql-datasource-mutation.md | 2 +- tests/execution/graphql-datasource-no-args.md | 2 +- .../execution/graphql-datasource-with-args.md | 4 +- tests/execution/grpc-batch.md | 13 +- tests/execution/grpc-error.md | 1 - .../grpc-override-url-from-upstream.md | 3 +- .../execution/grpc-proto-with-same-package.md | 6 +- tests/execution/grpc-reflection.md | 57 ++++ tests/execution/grpc-simple.md | 3 +- tests/execution/grpc-url-from-upstream.md | 3 +- tests/execution/https.md | 1 - tests/execution/inline-field.md | 1 - tests/execution/inline-index-list.md | 1 - tests/execution/io-cache.md | 5 +- tests/execution/jsonplaceholder-call-post.md | 4 +- tests/execution/modified-field.md | 1 - tests/execution/mutation-put.md | 2 +- tests/execution/mutation.md | 2 +- tests/execution/n-plus-one-list.md | 2 - tests/execution/n-plus-one.md | 2 - tests/execution/nested-objects.md | 1 - tests/execution/nesting-level3.md | 3 - tests/execution/nullable-arg-query.md | 2 - tests/execution/omit-index-list.md | 1 - tests/execution/omit-resolved-by-parent.md | 1 - tests/execution/recursive-type-json.md | 2 - tests/execution/recursive-types.md | 2 - tests/execution/ref-other-nested.md | 3 +- tests/execution/ref-other.md | 1 - tests/execution/rename-field.md | 2 - .../execution/request-to-upstream-batching.md | 1 - tests/execution/resolve-with-vars.md | 1 - tests/execution/resolved-by-parent.md | 1 - tests/execution/rest-api-error.md | 1 - tests/execution/rest-api-post.md | 1 - tests/execution/rest-api.md | 1 - tests/execution/showcase.md | 5 +- tests/execution/simple-graphql.md | 1 - tests/execution/simple-query.md | 1 - tests/execution/test-enum-default.md | 4 +- tests/execution/test-params-as-body.md | 2 +- tests/execution/test-static-value.md | 1 - tests/execution/test-upstream-headers.md | 1 - tests/execution/upstream-batching.md | 1 - tests/execution/upstream-fail-request.md | 1 - tests/execution/with-args-url.md | 1 - tests/execution/with-args.md | 1 - tests/execution/with-nesting.md | 2 - 113 files changed, 921 insertions(+), 379 deletions(-) create mode 100644 examples/grpc-reflection.graphql create mode 100644 src/proto_reader/fetch.rs create mode 100644 src/proto_reader/fixtures/descriptor_b64.txt create mode 100644 src/proto_reader/fixtures/response_b64.txt create mode 100644 src/proto_reader/mod.rs create mode 100644 src/proto_reader/proto/reflection.proto rename src/{proto_reader.rs => proto_reader/reader.rs} (71%) rename {src/grpc/tests => tailcall-fixtures/fixtures/configs}/proto_no_pkg.graphql (100%) rename {tailcall-query-plan/tests/config => tailcall-fixtures/fixtures/configs}/user-posts-query.graphql (100%) rename {tailcall-query-plan/tests/config => tailcall-fixtures/fixtures/configs}/user-posts.graphql (100%) rename {src => tailcall-fixtures/fixtures}/generator/proto/greetings.proto (100%) rename {src => tailcall-fixtures/fixtures}/generator/proto/greetings_a.proto (100%) rename {src => tailcall-fixtures/fixtures}/generator/proto/greetings_b.proto (100%) rename {src => tailcall-fixtures/fixtures}/generator/proto/greetings_message.proto (100%) rename {src => tailcall-fixtures/fixtures}/generator/proto/news.proto (100%) rename {src => tailcall-fixtures/fixtures}/generator/proto/news_no_service.proto (100%) rename {src => tailcall-fixtures/fixtures}/generator/proto/no_pkg.proto (100%) rename {src => tailcall-fixtures/fixtures}/generator/proto/person.proto (100%) create mode 100644 tailcall-fixtures/fixtures/grpc/reflection/news-list-services.bin create mode 100644 tailcall-fixtures/fixtures/grpc/reflection/news-service-descriptor.bin create mode 100644 tests/core/snapshots/grpc-reflection.md_0.snap create mode 100644 tests/core/snapshots/grpc-reflection.md_client.snap create mode 100644 tests/core/snapshots/grpc-reflection.md_merged.snap create mode 100644 tests/execution/grpc-reflection.md diff --git a/Cargo.lock b/Cargo.lock index ef9fc77ee7..98145dfd60 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5171,6 +5171,7 @@ dependencies = [ "async-recursion", "async-std", "async-trait", + "base64 0.22.0", "cache_control", "chrono", "clap", @@ -5383,6 +5384,7 @@ dependencies = [ "indexmap 2.2.6", "insta", "tailcall", + "tailcall-fixtures", "tokio", ] diff --git a/Cargo.toml b/Cargo.toml index a6a62d2353..fb1130f6f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -148,6 +148,7 @@ tailcall-macros = { path = "tailcall-macros" } tonic-types = "0.11.0" datatest-stable = "0.2.9" tokio-test = "0.4.4" +base64 = "0.22.0" [dev-dependencies] diff --git a/examples/grpc-reflection.graphql b/examples/grpc-reflection.graphql new file mode 100644 index 0000000000..1d96f10149 --- /dev/null +++ b/examples/grpc-reflection.graphql @@ -0,0 +1,36 @@ +# for test upstream server see [repo](https://github.com/tailcallhq/tailcall/tree/main/tailcall-upstream-grpc) +schema + @server(port: 8000, graphiql: true) + @upstream(baseURL: "http://localhost:50051", httpCache: true, batch: {delay: 10}) + @link(src: "http://localhost:50051", type: Grpc) { + query: Query +} + +type Query { + news: NewsData! @grpc(method: "news.NewsService.GetAllNews") + newsById(news: NewsInput!): News! @grpc(method: "news.NewsService.GetNews", body: "{{args.news}}") + newsByIdBatch(news: NewsInput!): News! + @grpc(method: "news.NewsService.GetMultipleNews", body: "{{args.news}}", batchKey: ["news", "id"]) +} + +type News { + id: Int + title: String + body: String + postImage: String + status: Status +} + +enum Status { + PUBLISHED + DRAFT + DELETED +} + +input NewsInput { + id: Int +} + +type NewsData { + news: [News]! +} diff --git a/examples/lint.sh b/examples/lint.sh index ae375988bf..c12b62f68b 100755 --- a/examples/lint.sh +++ b/examples/lint.sh @@ -14,9 +14,10 @@ check_files() { local depth=1 local -a extensions=("-name" "*.json" -o "-name" "*.yml" -o "-name" "*.yaml" -o "-name" "*.graphql" -o "-name" "*.gql") local command="./target/debug/tailcall check" + local -a ignore=("!" "-name" "grpc-reflection.graphql") # Execute find command with constructed options and extensions - find "$path" -maxdepth "$depth" \( "${extensions[@]}" \) -exec sh -c ' + find "$path" -maxdepth "$depth" \( "${extensions[@]}" \) "${ignore[@]}" -exec sh -c ' for file; do echo "Checking file: $file" '"$command"' "$file" || exit 255 diff --git a/generated/.tailcallrc.graphql b/generated/.tailcallrc.graphql index 72f0216808..cbba820756 100644 --- a/generated/.tailcallrc.graphql +++ b/generated/.tailcallrc.graphql @@ -682,6 +682,7 @@ enum LinkType { Operation Htpasswd Jwks + Grpc } enum Method { GET diff --git a/generated/.tailcallrc.schema.json b/generated/.tailcallrc.schema.json index c23b622061..265020e9f4 100644 --- a/generated/.tailcallrc.schema.json +++ b/generated/.tailcallrc.schema.json @@ -792,7 +792,8 @@ "Key", "Operation", "Htpasswd", - "Jwks" + "Jwks", + "Grpc" ] }, "Method": { diff --git a/src/config/link.rs b/src/config/link.rs index dc714d4386..d236148541 100644 --- a/src/config/link.rs +++ b/src/config/link.rs @@ -23,6 +23,7 @@ pub enum LinkType { Operation, Htpasswd, Jwks, + Grpc, } /// The @link directive allows you to import external resources, such as diff --git a/src/config/reader.rs b/src/config/reader.rs index c96f191f94..680e58d79d 100644 --- a/src/config/reader.rs +++ b/src/config/reader.rs @@ -28,7 +28,7 @@ impl ConfigReader { Self { runtime: runtime.clone(), resource_reader: resource_reader.clone(), - proto_reader: ProtoReader::init(resource_reader), + proto_reader: ProtoReader::init(resource_reader, runtime), } } @@ -59,12 +59,11 @@ impl ConfigReader { for link in links.iter() { let path = Self::resolve_path(&link.src, parent_dir); - let source = self.resource_reader.read_file(&path).await?; - - let content = source.content; - match link.type_of { LinkType::Config => { + let source = self.resource_reader.read_file(&path).await?; + let content = source.content; + let config = Config::from_source(Source::detect(&source.path)?, &content)?; config_module = config_module.merge_right(ConfigModule::from(config.clone())); @@ -80,33 +79,46 @@ impl ConfigReader { } } LinkType::Protobuf => { - let path = Self::resolve_path(&link.src, parent_dir); let meta = self.proto_reader.read(path).await?; config_module.extensions.add_proto(meta); } LinkType::Script => { + let source = self.resource_reader.read_file(&path).await?; + let content = source.content; config_module.extensions.script = Some(content); } LinkType::Cert => { + let source = self.resource_reader.read_file(&path).await?; + let content = source.content; config_module .extensions .cert - .extend(self.load_cert(content.clone()).await?); + .extend(self.load_cert(content).await?); } LinkType::Key => { - config_module.extensions.keys = - Arc::new(self.load_private_key(content.clone()).await?) + let source = self.resource_reader.read_file(&path).await?; + let content = source.content; + config_module.extensions.keys = Arc::new(self.load_private_key(content).await?) } LinkType::Operation => { + let source = self.resource_reader.read_file(&path).await?; + let content = source.content; + config_module.extensions.endpoint_set = EndpointSet::try_new(&content)?; } LinkType::Htpasswd => { + let source = self.resource_reader.read_file(&path).await?; + let content = source.content; + config_module .extensions .htpasswd - .push(Content { id: link.id.clone(), content: content.clone() }); + .push(Content { id: link.id.clone(), content }); } LinkType::Jwks => { + let source = self.resource_reader.read_file(&path).await?; + let content = source.content; + let de = &mut serde_json::Deserializer::from_str(&content); config_module.extensions.jwks.push(Content { @@ -114,6 +126,13 @@ impl ConfigReader { content: serde_path_to_error::deserialize(de)?, }) } + LinkType::Grpc => { + let meta = self.proto_reader.fetch(link.src.as_str()).await?; + + for m in meta { + config_module.extensions.add_proto(m); + } + } } } @@ -197,12 +216,13 @@ impl ConfigReader { let server = &mut config_module.config.server; let reader_ctx = ConfigReaderContext { - env: self.runtime.env.clone(), + runtime: &self.runtime, vars: &server .vars .iter() .map(|vars| (vars.key.clone(), vars.value.clone())) .collect(), + headers: Default::default(), }; config_module diff --git a/src/config/reader_context.rs b/src/config/reader_context.rs index dc1641fef3..8c16f4783b 100644 --- a/src/config/reader_context.rs +++ b/src/config/reader_context.rs @@ -1,13 +1,16 @@ use std::borrow::Cow; use std::collections::BTreeMap; -use std::sync::Arc; +use headers::HeaderMap; + +use crate::has_headers::HasHeaders; use crate::path::PathString; -use crate::EnvIO; +use crate::runtime::TargetRuntime; pub struct ConfigReaderContext<'a> { - pub env: Arc, + pub runtime: &'a TargetRuntime, pub vars: &'a BTreeMap, + pub headers: HeaderMap, } impl<'a> PathString for ConfigReaderContext<'a> { @@ -19,25 +22,37 @@ impl<'a> PathString for ConfigReaderContext<'a> { path.split_first() .and_then(|(head, tail)| match head.as_ref() { "vars" => self.vars.get(tail[0].as_ref()).map(|v| v.into()), - "env" => self.env.get(tail[0].as_ref()), + "env" => self.runtime.env.get(tail[0].as_ref()), _ => None, }) } } +impl HasHeaders for ConfigReaderContext<'_> { + fn headers(&self) -> &HeaderMap { + &self.headers + } +} + #[cfg(test)] mod tests { + use std::sync::Arc; + use super::*; use crate::tests::TestEnvIO; #[test] fn path_string() { + let mut runtime = crate::runtime::test::init(None); + runtime.env = Arc::new(TestEnvIO::from_iter([( + "ENV_1".to_owned(), + "ENV_VAL".to_owned(), + )])); + let reader_context = ConfigReaderContext { - env: Arc::new(TestEnvIO::from_iter([( - "ENV_1".to_owned(), - "ENV_VAL".to_owned(), - )])), + runtime: &runtime, vars: &BTreeMap::from_iter([("VAR_1".to_owned(), "VAR_VAL".to_owned())]), + headers: Default::default(), }; assert_eq!( diff --git a/src/generator/from_proto.rs b/src/generator/from_proto.rs index 75a4301c16..603d149fdc 100644 --- a/src/generator/from_proto.rs +++ b/src/generator/from_proto.rs @@ -270,8 +270,7 @@ mod test { use crate::generator::from_proto::from_proto; fn get_proto_file_descriptor(name: &str) -> anyhow::Result { - let path = - PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(format!("src/generator/proto/{}", name)); + let path = PathBuf::from(tailcall_fixtures::generator::proto::SELF).join(name); Ok(protox_parse::parse( name, std::fs::read_to_string(path)?.as_str(), diff --git a/src/generator/generator.rs b/src/generator/generator.rs index db359a92a4..ab0ee46314 100644 --- a/src/generator/generator.rs +++ b/src/generator/generator.rs @@ -14,7 +14,7 @@ pub struct Generator { impl Generator { pub fn init(runtime: TargetRuntime) -> Self { Self { - proto_reader: ProtoReader::init(ResourceReader::cached(runtime)), + proto_reader: ProtoReader::init(ResourceReader::cached(runtime.clone()), runtime), } } @@ -56,7 +56,7 @@ mod test { async fn test_read_all() { let server = start_mock_server(); let runtime = crate::runtime::test::init(None); - let test_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src/generator/proto"); + let test_dir = PathBuf::from(tailcall_fixtures::generator::proto::SELF); let news_content = runtime .file diff --git a/src/main.rs b/src/main.rs index 09a423a71f..ef0806c1d3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -32,6 +32,7 @@ fn run_blocking() -> anyhow::Result<()> { .on_thread_stop(|| { TRACING_GUARD.take(); }) + .enable_all() .build()?; rt.block_on(async { tailcall::cli::run().await }) } diff --git a/src/proto_reader/fetch.rs b/src/proto_reader/fetch.rs new file mode 100644 index 0000000000..deabef7d0f --- /dev/null +++ b/src/proto_reader/fetch.rs @@ -0,0 +1,310 @@ +use anyhow::{Context, Result}; +use base64::prelude::BASE64_STANDARD; +use base64::Engine; +use hyper::header::HeaderName; +use nom::AsBytes; +use prost::Message; +use prost_reflect::prost_types::{FileDescriptorProto, FileDescriptorSet}; +use serde::{Deserialize, Serialize}; +use serde_json::json; + +use crate::blueprint::GrpcMethod; +use crate::config::ConfigReaderContext; +use crate::grpc::protobuf::ProtobufSet; +use crate::grpc::RequestTemplate; +use crate::mustache::Mustache; +use crate::runtime::TargetRuntime; + +/// +/// Loading reflection proto +/// https://github.com/grpc/grpc/blob/master/src/proto/grpc/reflection/v1alpha/reflection.proto +const REFLECTION_PROTO: &str = include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/src/proto_reader/proto/reflection.proto" +)); + +/// This function is just used for better exception handling +fn get_protobuf_set() -> Result { + let descriptor = protox_parse::parse("reflection", REFLECTION_PROTO)?; + let mut descriptor_set = FileDescriptorSet::default(); + descriptor_set.file.push(descriptor); + ProtobufSet::from_proto_file(descriptor_set) +} + +#[derive(Debug, Serialize, Deserialize)] +struct Service { + name: String, +} + +#[derive(Debug, Serialize, Deserialize)] +struct ListServicesResponse { + service: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +struct FileDescriptorProtoResponse { + file_descriptor_proto: Vec, +} + +impl FileDescriptorProtoResponse { + fn get(self) -> Result> { + let file_descriptor_proto = self + .file_descriptor_proto + .first() + .context("Received empty fileDescriptorProto")?; + + BASE64_STANDARD + .decode(file_descriptor_proto) + .context("Failed to decode fileDescriptorProto from BASE64") + } +} + +/// Used for serializing all kinds of GRPC Reflection responses +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +struct ReflectionResponse { + list_services_response: Option, + file_descriptor_response: Option, +} + +pub struct GrpcReflection { + server_reflection_method: GrpcMethod, + url: String, + target_runtime: TargetRuntime, +} + +impl GrpcReflection { + pub fn new>(url: T, target_runtime: TargetRuntime) -> Self { + let server_reflection_method = GrpcMethod { + package: "grpc.reflection.v1alpha".to_string(), + service: "ServerReflection".to_string(), + name: "ServerReflectionInfo".to_string(), + }; + Self { + server_reflection_method, + url: url.as_ref().to_string(), + target_runtime, + } + } + /// Makes `ListService` request to the grpc reflection server + pub async fn list_all_files(&self) -> Result> { + // Extracting names from services + let methods: Vec = self + .execute(json!({"list_services": ""})) + .await? + .list_services_response + .context("Couldn't find definitions for service ServerReflection")? + .service + .iter() + .map(|s| s.name.clone()) + .collect(); + + Ok(methods) + } + + /// Makes `Get Service` request to the grpc reflection server + pub async fn get_by_service(&self, service: &str) -> Result { + let resp = self + .execute(json!({"file_containing_symbol": service})) + .await?; + + request_proto(resp).await + } + + async fn execute(&self, body: serde_json::Value) -> Result { + let server_reflection_method = &self.server_reflection_method; + let protobuf_set = get_protobuf_set()?; + let reflection_service = protobuf_set.find_service(server_reflection_method)?; + let operation = reflection_service.find_operation(server_reflection_method)?; + let mut url: url::Url = self.url.parse()?; + url.set_path( + format!( + "{}.{}/{}", + server_reflection_method.package, + server_reflection_method.service, + server_reflection_method.name + ) + .as_str(), + ); + let req_template = RequestTemplate { + url: Mustache::parse(url.as_str())?, + headers: vec![( + HeaderName::from_static("content-type"), + Mustache::parse("application/grpc+proto")?, + )], + body: Mustache::parse(body.to_string().as_str()).ok(), + operation: operation.clone(), + operation_type: Default::default(), + }; + + let ctx = ConfigReaderContext { + runtime: &self.target_runtime, + vars: &Default::default(), + headers: Default::default(), + }; + + let req = req_template.render(&ctx)?.to_request()?; + + let resp = self.target_runtime.http.execute(req).await?; + let body = resp.body.as_bytes(); + + let response: ReflectionResponse = operation.convert_output(body)?; + Ok(response) + } +} + +/// For extracting `FileDescriptorProto` from `CustomResponse` +async fn request_proto(response: ReflectionResponse) -> Result { + let file_descriptor_resp = response + .file_descriptor_response + .context("Expected fileDescriptorResponse but found none")?; + let file_descriptor_proto = + FileDescriptorProto::decode(file_descriptor_resp.get()?.as_bytes())?; + + Ok(file_descriptor_proto) +} + +#[cfg(test)] +mod grpc_fetch { + use std::path::PathBuf; + + use anyhow::Result; + + use super::*; + + fn get_fake_descriptor() -> Vec { + let mut path = PathBuf::from(file!()); + path.pop(); + path.push("fixtures/descriptor_b64.txt"); + + let bytes = std::fs::read(path).unwrap(); + + BASE64_STANDARD.decode(bytes).unwrap() + } + + fn get_fake_resp() -> Vec { + let mut path = PathBuf::from(file!()); + path.pop(); + path.push("fixtures/response_b64.txt"); + + let bytes = std::fs::read(path).unwrap(); + + BASE64_STANDARD.decode(bytes).unwrap() + } + + fn start_mock_server() -> httpmock::MockServer { + httpmock::MockServer::start() + } + + #[tokio::test] + async fn test_resp_service() -> Result<()> { + let server = start_mock_server(); + + let http_reflection_file_mock = server.mock(|when, then| { + when.method(httpmock::Method::POST) + .path("/grpc.reflection.v1alpha.ServerReflection/ServerReflectionInfo") + .body("\0\0\0\0\x12\"\x10news.NewsService"); + then.status(200).body(get_fake_descriptor()); + }); + + let grpc_reflection = GrpcReflection::new( + format!("http://localhost:{}", server.port()), + crate::runtime::test::init(None), + ); + + let runtime = crate::runtime::test::init(None); + let resp = grpc_reflection.get_by_service("news.NewsService").await?; + + let content = runtime.file.read(tailcall_fixtures::protobuf::NEWS).await?; + let expected = protox_parse::parse("news.proto", &content)?; + + assert_eq!(expected.name(), resp.name()); + + http_reflection_file_mock.assert(); + Ok(()) + } + + #[tokio::test] + async fn test_resp_list_all() -> Result<()> { + let server = start_mock_server(); + + let http_reflection_list_all = server.mock(|when, then| { + when.method(httpmock::Method::POST) + .path("/grpc.reflection.v1alpha.ServerReflection/ServerReflectionInfo") + .body("\0\0\0\0\x02:\0"); + then.status(200).body(get_fake_resp()); + }); + + let runtime = crate::runtime::test::init(None); + + let grpc_reflection = + GrpcReflection::new(format!("http://localhost:{}", server.port()), runtime); + + let resp = grpc_reflection.list_all_files().await?; + + assert_eq!( + [ + "news.NewsService".to_string(), + "grpc.reflection.v1alpha.ServerReflection".to_string() + ] + .to_vec(), + resp + ); + + http_reflection_list_all.assert(); + + Ok(()) + } + + #[tokio::test] + async fn test_list_all_files_empty_response() -> Result<()> { + let server = start_mock_server(); + + let http_reflection_list_all_empty = server.mock(|when, then| { + when.method(httpmock::Method::POST) + .path("/grpc.reflection.v1alpha.ServerReflection/ServerReflectionInfo") + .body("\0\0\0\0\x02:\0"); + then.status(200).body("\0\0\0\0\x02:\0"); // Mock an empty response + }); + + let runtime = crate::runtime::test::init(None); + + let grpc_reflection = + GrpcReflection::new(format!("http://localhost:{}", server.port()), runtime); + + let resp = grpc_reflection.list_all_files().await; + + assert_eq!( + "Couldn't find definitions for service ServerReflection", + resp.err().unwrap().to_string() + ); + + http_reflection_list_all_empty.assert(); + + Ok(()) + } + + #[tokio::test] + async fn test_get_by_service_not_found() -> Result<()> { + let server = start_mock_server(); + + let http_reflection_service_not_found = server.mock(|when, then| { + when.method(httpmock::Method::POST) + .path("/grpc.reflection.v1alpha.ServerReflection/ServerReflectionInfo"); + then.status(404); // Mock a 404 not found response + }); + + let runtime = crate::runtime::test::init(None); + + let grpc_reflection = + GrpcReflection::new(format!("http://localhost:{}", server.port()), runtime); + + let result = grpc_reflection.get_by_service("nonexistent.Service").await; + assert!(result.is_err()); + + http_reflection_service_not_found.assert(); + + Ok(()) + } +} diff --git a/src/proto_reader/fixtures/descriptor_b64.txt b/src/proto_reader/fixtures/descriptor_b64.txt new file mode 100644 index 0000000000..8f93ea0ced --- /dev/null +++ b/src/proto_reader/fixtures/descriptor_b64.txt @@ -0,0 +1 @@ +AAAABVgSEiIQbmV3cy5OZXdzU2VydmljZSLBCgq+CgoKbmV3cy5wcm90bxIEbmV3cxobZ29vZ2xlL3Byb3RvYnVmL2VtcHR5LnByb3RvIkIKBE5ld3MSCgoCaWQYASABKAUSDQoFdGl0bGUYAiABKAkSDAoEYm9keRgDIAEoCRIRCglwb3N0SW1hZ2UYBCABKAkiFAoGTmV3c0lkEgoKAmlkGAEgASgFIiMKDk11bHRpcGxlTmV3c0lkEhEKA2lkcxgBIAMyBk5ld3NJZCIcCghOZXdzTGlzdBIQCgRuZXdzGAEgAzIETmV3czLeAQoLTmV3c1NlcnZpY2USLQoKR2V0QWxsTmV3cxIVZ29vZ2xlLnByb3RvYnVmLkVtcHR5GghOZXdzTGlzdBIXCgdHZXROZXdzEgZOZXdzSWQaBE5ld3MSKwoPR2V0TXVsdGlwbGVOZXdzEg5NdWx0aXBsZU5ld3NJZBoITmV3c0xpc3QSKwoKRGVsZXRlTmV3cxIGTmV3c0lkGhVnb29nbGUucHJvdG9idWYuRW1wdHkSFgoIRWRpdE5ld3MSBE5ld3MaBE5ld3MSFQoHQWRkTmV3cxIETmV3cxoETmV3c0qGBwoGEgQAACABCggKAQISAwQADQoJCgIDABIDAgAlCgoKAgQAEgQGAAsBCgoKAwQAARIDBggMCgsKBAQAAgASAwcEEQoMCgUEAAIAARIDBwoMCgwKBQQAAgADEgMHDxAKDAoFBAACAAUSAwcECQoLCgQEAAIBEgMIBBUKDAoFBAACAQESAwgLEAoMCgUEAAIBAxIDCBMUCgwKBQQAAgEFEgMIBAoKCwoEBAACAhIDCQQUCgwKBQQAAgIBEgMJCw8KDAoFBAACAgMSAwkSEwoMCgUEAAICBRIDCQQKCgsKBAQAAgMSAwoEGQoMCgUEAAIDARIDCgsUCgwKBQQAAgMDEgMKFxgKDAoFBAACAwUSAwoECgoKCgIEARIEFgAYAQoKCgMEAQESAxYIDgoLCgQEAQIAEgMXBBEKDAoFBAECAAESAxcKDAoMCgUEAQIAAxIDFw8QCgwKBQQBAgAFEgMXBAkKCgoCBAISBBoAHAEKCgoDBAIBEgMaCBYKCwoEBAICABIDGwQcCgwKBQQCAgABEgMbFBcKDAoFBAICAAMSAxsaGwoMCgUEAgIABBIDGwQMCgwKBQQCAgAGEgMbDRMKCgoCBAMSBB4AIAEKCgoDBAMBEgMeCBAKCwoEBAMCABIDHwMaCgwKBQQDAgABEgMfERUKDAoFBAMCAAMSAx8YGQoMCgUEAwIABBIDHwMLCgwKBQQDAgAGEgMfDBAKCgoCBgASBA0AFAEKCgoDBgABEgMNCBMKCwoEBgACABIDDgRACgwKBQYAAgABEgMOCBIKDAoFBgACAAISAw4UKQoMCgUGAAIAAxIDDjQ8CgsKBAYAAgESAw8EKgoMCgUGAAIBARIDDwgPCgwKBQYAAgECEgMPERcKDAoFBgACAQMSAw8iJgoLCgQGAAICEgMQBD4KDAoFBgACAgESAxAIFwoMCgUGAAICAhIDEBknCgwKBQYAAgIDEgMQMjoKCwoEBgACAxIDEQQ+CgwKBQYAAgMBEgMRCBIKDAoFBgACAwISAxEUGgoMCgUGAAIDAxIDESU6CgsKBAYAAgQSAxIEKQoMCgUGAAIEARIDEggQCgwKBQYAAgQCEgMSEhYKDAoFBgACBAMSAxIhJQoLCgQGAAIFEgMTBCgKDAoFBgACBQESAxMIDwoMCgUGAAIFAhIDExEVCgwKBQYAAgUDEgMTICQKCAoBDBIDAAASYgZwcm90bzM= \ No newline at end of file diff --git a/src/proto_reader/fixtures/response_b64.txt b/src/proto_reader/fixtures/response_b64.txt new file mode 100644 index 0000000000..a989c3aec2 --- /dev/null +++ b/src/proto_reader/fixtures/response_b64.txt @@ -0,0 +1 @@ +AAAAAEYSAjoAMkAKEgoQbmV3cy5OZXdzU2VydmljZQoqCihncnBjLnJlZmxlY3Rpb24udjFhbHBoYS5TZXJ2ZXJSZWZsZWN0aW9u \ No newline at end of file diff --git a/src/proto_reader/mod.rs b/src/proto_reader/mod.rs new file mode 100644 index 0000000000..06af656e55 --- /dev/null +++ b/src/proto_reader/mod.rs @@ -0,0 +1,3 @@ +pub use reader::*; +mod fetch; +mod reader; diff --git a/src/proto_reader/proto/reflection.proto b/src/proto_reader/proto/reflection.proto new file mode 100644 index 0000000000..462e85a222 --- /dev/null +++ b/src/proto_reader/proto/reflection.proto @@ -0,0 +1,136 @@ +// Copyright 2016 gRPC authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Service exported by server reflection + +syntax = "proto3"; + +package grpc.reflection.v1alpha; + +service ServerReflection { + // The reflection service is structured as a bidirectional stream, ensuring + // all related requests go to a single server. + rpc ServerReflectionInfo(stream ServerReflectionRequest) + returns (stream ServerReflectionResponse); +} + +// The message sent by the client when calling ServerReflectionInfo method. +message ServerReflectionRequest { + string host = 1; + // To use reflection service, the client should set one of the following + // fields in message_request. The server distinguishes requests by their + // defined field and then handles them using corresponding methods. + oneof message_request { + // Find a proto file by the file name. + string file_by_filename = 3; + + // Find the proto file that declares the given fully-qualified symbol name. + // This field should be a fully-qualified symbol name + // (e.g. .[.] or .). + string file_containing_symbol = 4; + + // Find the proto file which defines an extension extending the given + // message type with the given field number. + ExtensionRequest file_containing_extension = 5; + + // Finds the tag numbers used by all known extensions of the given message + // type, and appends them to ExtensionNumberResponse in an undefined order. + // Its corresponding method is best-effort: it's not guaranteed that the + // reflection service will implement this method, and it's not guaranteed + // that this method will provide all extensions. Returns + // StatusCode::UNIMPLEMENTED if it's not implemented. + // This field should be a fully-qualified type name. The format is + // . + string all_extension_numbers_of_type = 6; + + // List the full names of registered services. The content will not be + // checked. + string list_services = 7; + } +} + +// The type name and extension number sent by the client when requesting +// file_containing_extension. +message ExtensionRequest { + // Fully-qualified type name. The format should be . + string containing_type = 1; + int32 extension_number = 2; +} + +// The message sent by the server to answer ServerReflectionInfo method. +message ServerReflectionResponse { + string valid_host = 1; + ServerReflectionRequest original_request = 2; + // The server set one of the following fields accroding to the message_request + // in the request. + oneof message_response { + // This message is used to answer file_by_filename, file_containing_symbol, + // file_containing_extension requests with transitive dependencies. As + // the repeated label is not allowed in oneof fields, we use a + // FileDescriptorResponse message to encapsulate the repeated fields. + // The reflection service is allowed to avoid sending FileDescriptorProtos + // that were previously sent in response to earlier requests in the stream. + FileDescriptorResponse file_descriptor_response = 4; + + // This message is used to answer all_extension_numbers_of_type requst. + ExtensionNumberResponse all_extension_numbers_response = 5; + + // This message is used to answer list_services request. + ListServiceResponse list_services_response = 6; + + // This message is used when an error occurs. + ErrorResponse error_response = 7; + } +} + +// Serialized FileDescriptorProto messages sent by the server answering +// a file_by_filename, file_containing_symbol, or file_containing_extension +// request. +message FileDescriptorResponse { + // Serialized FileDescriptorProto messages. We avoid taking a dependency on + // descriptor.proto, which uses proto2 only features, by making them opaque + // bytes instead. + repeated bytes file_descriptor_proto = 1; +} + +// A list of extension numbers sent by the server answering +// all_extension_numbers_of_type request. +message ExtensionNumberResponse { + // Full name of the base type, including the package name. The format + // is . + string base_type_name = 1; + repeated int32 extension_number = 2; +} + +// A list of ServiceResponse sent by the server answering list_services request. +message ListServiceResponse { + // The information of each service may be expanded in the future, so we use + // ServiceResponse message to encapsulate it. + repeated ServiceResponse service = 1; +} + +// The information of a single service used by ListServiceResponse to answer +// list_services request. +message ServiceResponse { + // Full name of a registered service, including its package name. The format + // is . + string name = 1; +} + +// The error code and error message sent by the server when an error occurs. +message ErrorResponse { + // This field uses the error codes defined in grpc::StatusCode. + int32 error_code = 1; + string error_message = 2; +} \ No newline at end of file diff --git a/src/proto_reader.rs b/src/proto_reader/reader.rs similarity index 71% rename from src/proto_reader.rs rename to src/proto_reader/reader.rs index 8408dcd006..8b3327f06a 100644 --- a/src/proto_reader.rs +++ b/src/proto_reader/reader.rs @@ -6,10 +6,13 @@ use futures_util::future::join_all; use prost_reflect::prost_types::{FileDescriptorProto, FileDescriptorSet}; use protox::file::{FileResolver, GoogleFileResolver}; +use crate::proto_reader::fetch::GrpcReflection; use crate::resource_reader::{Cached, ResourceReader}; +use crate::runtime::TargetRuntime; pub struct ProtoReader { - resource_reader: ResourceReader, + reader: ResourceReader, + runtime: TargetRuntime, } pub struct ProtoMetadata { @@ -18,10 +21,34 @@ pub struct ProtoMetadata { } impl ProtoReader { - pub fn init(resource_reader: ResourceReader) -> Self { - Self { resource_reader } + /// Initializes the proto reader with a resource reader and target runtime + pub fn init(reader: ResourceReader, runtime: TargetRuntime) -> Self { + Self { reader, runtime } } + /// Fetches proto files from a grpc server (grpc reflection) + pub async fn fetch>(&self, url: T) -> anyhow::Result> { + let grpc_reflection = GrpcReflection::new(url.as_ref(), self.runtime.clone()); + + let mut proto_metadata = vec![]; + let service_list = grpc_reflection.list_all_files().await?; + for service in service_list { + if service.eq("grpc.reflection.v1alpha.ServerReflection") { + continue; + } + let file_descriptor_proto = grpc_reflection.get_by_service(&service).await?; + Self::check_package(&file_descriptor_proto)?; + let descriptors = self.resolve(file_descriptor_proto, None).await?; + let metadata = ProtoMetadata { + descriptor_set: FileDescriptorSet { file: descriptors }, + path: url.as_ref().to_string(), + }; + proto_metadata.push(metadata); + } + Ok(proto_metadata) + } + + /// Asynchronously reads all proto files from a list of paths pub async fn read_all>(&self, paths: &[T]) -> anyhow::Result> { let resolved_protos = join_all(paths.iter().map(|v| self.read(v.as_ref()))) .await @@ -30,14 +57,13 @@ impl ProtoReader { Ok(resolved_protos) } + /// Reads a proto file from a path pub async fn read>(&self, path: T) -> anyhow::Result { let file_read = self.read_proto(path.as_ref(), None).await?; - if file_read.package.is_none() { - anyhow::bail!("Package name is required"); - } + Self::check_package(&file_read)?; let descriptors = self - .resolve_descriptors(file_read, PathBuf::from(path.as_ref()).parent()) + .resolve(file_read, PathBuf::from(path.as_ref()).parent()) .await?; let metadata = ProtoMetadata { descriptor_set: FileDescriptorSet { file: descriptors }, @@ -47,7 +73,7 @@ impl ProtoReader { } /// Performs BFS to import all nested proto files - async fn resolve_descriptors( + async fn resolve( &self, parent_proto: FileDescriptorProto, parent_path: Option<&Path>, @@ -94,7 +120,7 @@ impl ProtoReader { .to_string() } else { let path = Self::resolve_path(path.as_ref(), parent_dir); - self.resource_reader.read_file(path).await?.content + self.reader.read_file(path).await?.content }; Ok(protox_parse::parse(path.as_ref(), &content)?) } @@ -113,6 +139,12 @@ impl ProtoReader { src.to_string() } } + fn check_package(proto: &FileDescriptorProto) -> anyhow::Result<()> { + if proto.package.is_none() { + anyhow::bail!("Package name is required"); + } + Ok(()) + } } #[cfg(test)] @@ -129,9 +161,8 @@ mod test_proto_config { #[tokio::test] async fn test_resolve() { // Skipping IO tests as they are covered in reader.rs - let reader = ProtoReader::init(ResourceReader::::cached( - crate::runtime::test::init(None), - )); + let runtime = crate::runtime::test::init(None); + let reader = ProtoReader::init(ResourceReader::::cached(runtime.clone()), runtime); reader .read_proto("google/protobuf/empty.proto", None) .await @@ -146,9 +177,9 @@ mod test_proto_config { let runtime = crate::runtime::test::init(None); let file_rt = runtime.file.clone(); - let reader = ProtoReader::init(ResourceReader::::cached(runtime)); + let reader = ProtoReader::init(ResourceReader::::cached(runtime.clone()), runtime); let helper_map = reader - .resolve_descriptors(reader.read_proto(&test_file, None).await?, Some(test_dir)) + .resolve(reader.read_proto(&test_file, None).await?, Some(test_dir)) .await?; let files = test_dir.read_dir()?; for file in files { @@ -170,9 +201,9 @@ mod test_proto_config { #[tokio::test] async fn test_proto_no_pkg() -> Result<()> { let runtime = crate::runtime::test::init(None); - let reader = ProtoReader::init(ResourceReader::::cached(runtime)); - let mut proto_no_pkg = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - proto_no_pkg.push("src/grpc/tests/proto_no_pkg.graphql"); + let reader = ProtoReader::init(ResourceReader::::cached(runtime.clone()), runtime); + let proto_no_pkg = + PathBuf::from(tailcall_fixtures::configs::SELF).join("proto_no_pkg.graphql"); let config_module = reader.read(proto_no_pkg.to_str().unwrap()).await; assert!(config_module.is_err()); Ok(()) diff --git a/src/grpc/tests/proto_no_pkg.graphql b/tailcall-fixtures/fixtures/configs/proto_no_pkg.graphql similarity index 100% rename from src/grpc/tests/proto_no_pkg.graphql rename to tailcall-fixtures/fixtures/configs/proto_no_pkg.graphql diff --git a/tailcall-query-plan/tests/config/user-posts-query.graphql b/tailcall-fixtures/fixtures/configs/user-posts-query.graphql similarity index 100% rename from tailcall-query-plan/tests/config/user-posts-query.graphql rename to tailcall-fixtures/fixtures/configs/user-posts-query.graphql diff --git a/tailcall-query-plan/tests/config/user-posts.graphql b/tailcall-fixtures/fixtures/configs/user-posts.graphql similarity index 100% rename from tailcall-query-plan/tests/config/user-posts.graphql rename to tailcall-fixtures/fixtures/configs/user-posts.graphql diff --git a/src/generator/proto/greetings.proto b/tailcall-fixtures/fixtures/generator/proto/greetings.proto similarity index 100% rename from src/generator/proto/greetings.proto rename to tailcall-fixtures/fixtures/generator/proto/greetings.proto diff --git a/src/generator/proto/greetings_a.proto b/tailcall-fixtures/fixtures/generator/proto/greetings_a.proto similarity index 100% rename from src/generator/proto/greetings_a.proto rename to tailcall-fixtures/fixtures/generator/proto/greetings_a.proto diff --git a/src/generator/proto/greetings_b.proto b/tailcall-fixtures/fixtures/generator/proto/greetings_b.proto similarity index 100% rename from src/generator/proto/greetings_b.proto rename to tailcall-fixtures/fixtures/generator/proto/greetings_b.proto diff --git a/src/generator/proto/greetings_message.proto b/tailcall-fixtures/fixtures/generator/proto/greetings_message.proto similarity index 100% rename from src/generator/proto/greetings_message.proto rename to tailcall-fixtures/fixtures/generator/proto/greetings_message.proto diff --git a/src/generator/proto/news.proto b/tailcall-fixtures/fixtures/generator/proto/news.proto similarity index 100% rename from src/generator/proto/news.proto rename to tailcall-fixtures/fixtures/generator/proto/news.proto diff --git a/src/generator/proto/news_no_service.proto b/tailcall-fixtures/fixtures/generator/proto/news_no_service.proto similarity index 100% rename from src/generator/proto/news_no_service.proto rename to tailcall-fixtures/fixtures/generator/proto/news_no_service.proto diff --git a/src/generator/proto/no_pkg.proto b/tailcall-fixtures/fixtures/generator/proto/no_pkg.proto similarity index 100% rename from src/generator/proto/no_pkg.proto rename to tailcall-fixtures/fixtures/generator/proto/no_pkg.proto diff --git a/src/generator/proto/person.proto b/tailcall-fixtures/fixtures/generator/proto/person.proto similarity index 100% rename from src/generator/proto/person.proto rename to tailcall-fixtures/fixtures/generator/proto/person.proto diff --git a/tailcall-fixtures/fixtures/grpc/reflection/news-list-services.bin b/tailcall-fixtures/fixtures/grpc/reflection/news-list-services.bin new file mode 100644 index 0000000000000000000000000000000000000000..b284fe8164e7549596449a31577581a5eeb8a9bf GIT binary patch literal 75 zcmZQzU|?_)VzOc|a^Mo;639y}FV^z|62YlOWtqvTTv}Wj=|u&}dPS*eIjPAdnfZBo TWrm441sREYASJ0qL2wBG>~9y0 literal 0 HcmV?d00001 diff --git a/tailcall-fixtures/fixtures/grpc/reflection/news-service-descriptor.bin b/tailcall-fixtures/fixtures/grpc/reflection/news-service-descriptor.bin new file mode 100644 index 0000000000000000000000000000000000000000..b1e2f259743ad8c5e3daeba7af265cc63afdfc14 GIT binary patch literal 1373 zcmZuxU2oGs5Ipme5kIz%|2JfFQT7BWX_jYi+Z6I zKaVFDi^avfIx=VH*I$mR%Vm9YXkhV@6nkhQe^!pXU2i|oT?*=1J+DSSV5~c&&KKp) zNE(z;yj-m6v&&Bx)kyJC(Wj6-&dN=f;tuuRUC-;;a$Z?BBRMNqXd;iTpxB{^$+xpr zty41d8r}?b{M(~8(>9@gMD)6N_2fTGiK*7P*JY{xBXos^{+W#jnGj~0bm39;)@M4qpu z5?zYPmqJTl>|vX}5?Y0#?co`$r)3y*s7*=l`fYBKYLgQ#bw@Hq3(W(EK?gq$-2!ER_X1Ho>H%xvdtH5NOK9Zx=v2FA{3 zyNTK{1^}QYeKrgf5bP%7i9?Djn^xdLpl)u=rB=6ifxI8icPmPbITonpRE~`S0H|r! zTssiVr=#XtfM7nAZC45NX(wg#5X|G*)% z>OFa8WhhM4Lrd}1CXv}9O!)mvG83N>WJz()%0Q*+xs~x1rjaZ3ncKeaILOlDr&b1# ztz~>U*?LF`S=KkVgKqpPWP47A>(I*0kC#De+-Z{(yObKAnMIK4+k3VfHlC|}O9hbS e>udpJd2{>d7C@Ho-sNwuqook~JT(7?kN*HlB&XZ} literal 0 HcmV?d00001 diff --git a/tailcall-query-plan/Cargo.toml b/tailcall-query-plan/Cargo.toml index 9f29b81c8f..ece9e5a9c5 100644 --- a/tailcall-query-plan/Cargo.toml +++ b/tailcall-query-plan/Cargo.toml @@ -17,3 +17,4 @@ tailcall = { path = ".." } [dev-dependencies] insta = { workspace = true } +tailcall-fixtures = { path = "../tailcall-fixtures"} diff --git a/tailcall-query-plan/tests/execution.rs b/tailcall-query-plan/tests/execution.rs index 0586d7b028..698d389bd3 100644 --- a/tailcall-query-plan/tests/execution.rs +++ b/tailcall-query-plan/tests/execution.rs @@ -12,7 +12,7 @@ use tailcall_query_plan::plan::{GeneralPlan, OperationPlan}; #[tokio::test] async fn test_simple() { - let root_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/config"); + let root_dir = Path::new(tailcall_fixtures::configs::SELF); let config = fs::read_to_string(root_dir.join("user-posts.graphql")).unwrap(); let config = Config::from_sdl(&config).to_result().unwrap(); let config = ConfigModule::from(config); diff --git a/tests/core/http.rs b/tests/core/http.rs index 2a0d13a594..21d615e64a 100644 --- a/tests/core/http.rs +++ b/tests/core/http.rs @@ -1,6 +1,5 @@ extern crate core; -use std::panic; use std::path::Path; use std::str::FromStr; use std::sync::atomic::{AtomicUsize, Ordering}; @@ -9,7 +8,6 @@ use std::sync::Arc; use anyhow::anyhow; use hyper::body::Bytes; use reqwest::header::{HeaderName, HeaderValue}; -use serde_json::Value; use tailcall::http::Response; use tailcall::HttpIO; @@ -54,40 +52,9 @@ impl Http { } } -fn string_to_bytes(input: &str) -> Vec { - let mut bytes = Vec::new(); - let mut chars = input.chars().peekable(); - - while let Some(c) = chars.next() { - match c { - '\\' => match chars.next() { - Some('0') => bytes.push(0), - Some('n') => bytes.push(b'\n'), - Some('t') => bytes.push(b'\t'), - Some('r') => bytes.push(b'\r'), - Some('\\') => bytes.push(b'\\'), - Some('\"') => bytes.push(b'\"'), - Some('x') => { - let mut hex = chars.next().unwrap().to_string(); - hex.push(chars.next().unwrap()); - let byte = u8::from_str_radix(&hex, 16).unwrap(); - bytes.push(byte); - } - _ => panic!("Unsupported escape sequence"), - }, - _ => bytes.push(c as u8), - } - } - - bytes -} - #[async_trait::async_trait] impl HttpIO for Http { async fn execute(&self, req: reqwest::Request) -> anyhow::Result> { - // Determine if the request is a GRPC request based on PORT - let is_grpc = req.url().as_str().contains("50051"); - // Try to find a matching mock for the incoming request. let execution_mock = self .mocks @@ -96,21 +63,19 @@ impl HttpIO for Http { let mock_req = &mock.mock.request; let method_match = req.method() == mock_req.0.method.clone().to_hyper(); let url_match = req.url().as_str() == mock_req.0.url.clone().as_str(); - let req_body = match req.body() { - Some(body) => { - if let Some(bytes) = body.as_bytes() { - if let Ok(body_str) = std::str::from_utf8(bytes) { - Value::from(body_str) - } else { - Value::Null - } - } else { - Value::Null - } - } - None => Value::Null, - }; - let body_match = req_body == mock_req.0.body; + let body_match = mock_req + .0 + .body + .as_ref() + .map(|body| { + let mock_body = body.to_bytes(); + + req.body() + .and_then(|body| body.as_bytes().map(|req_body| req_body == mock_body)) + .unwrap_or(false) + }) + .unwrap_or(true); + let headers_match = req .headers() .iter() @@ -127,7 +92,7 @@ impl HttpIO for Http { .unwrap_or(&mock_header_value); header_value == mock_header_value }); - method_match && url_match && headers_match && (body_match || is_grpc) + method_match && url_match && headers_match && body_match }) .ok_or(anyhow!( "No mock found for request: {:?} {} in {}", @@ -158,17 +123,8 @@ impl HttpIO for Http { } // Special Handling for GRPC - if let Some(body) = mock_response.0.text_body { - // Return plaintext body if specified - let body = string_to_bytes(&body); - response.body = Bytes::from_iter(body); - } else if is_grpc { - // Special Handling for GRPC - let body = string_to_bytes(mock_response.0.body.as_str().unwrap_or_default()); - response.body = Bytes::from_iter(body); - } else { - let body = serde_json::to_vec(&mock_response.0.body)?; - response.body = Bytes::from_iter(body); + if let Some(body) = mock_response.0.body { + response.body = Bytes::from(body.to_bytes()); } Ok(response) diff --git a/tests/core/mod.rs b/tests/core/mod.rs index 71e3d53574..87866778a3 100644 --- a/tests/core/mod.rs +++ b/tests/core/mod.rs @@ -1,6 +1,6 @@ mod env; mod file; -mod http; +pub mod http; mod model; mod parse; mod runtime; diff --git a/tests/core/model.rs b/tests/core/model.rs index 77167876a6..0660493267 100644 --- a/tests/core/model.rs +++ b/tests/core/model.rs @@ -1,6 +1,9 @@ use std::collections::BTreeMap; +use std::panic; +use std::path::Path; use serde::{Deserialize, Serialize}; +use serde_json::Value; use tailcall::http::Method; use url::Url; @@ -8,7 +11,6 @@ use url::Url; #[serde(rename_all = "camelCase")] pub enum Annotation { Skip, - Only, } #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] @@ -17,11 +19,28 @@ pub struct UpstreamRequest(pub APIRequest); #[derive(Serialize, Deserialize, Clone, Debug)] pub struct UpstreamResponse(pub APIResponse); +mod default { + pub fn status() -> u16 { + 200 + } + + pub fn expected_hits() -> usize { + 1 + } + + pub fn assert_hits() -> bool { + true + } +} + #[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] pub struct Mock { pub request: UpstreamRequest, pub response: UpstreamResponse, - #[serde(default = "default_expected_hits")] + #[serde(default = "default::assert_hits")] + pub assert_hits: bool, + #[serde(default = "default::expected_hits")] pub expected_hits: usize, } @@ -32,8 +51,8 @@ pub struct APIRequest { pub url: Url, #[serde(default)] pub headers: BTreeMap, - #[serde(default)] - pub body: serde_json::Value, + #[serde(flatten, default)] + pub body: Option, #[serde(default)] pub test_traces: bool, #[serde(default)] @@ -43,21 +62,74 @@ pub struct APIRequest { #[derive(Serialize, Deserialize, Clone, Debug)] #[serde(rename_all = "camelCase")] pub struct APIResponse { - #[serde(default = "default_status")] + #[serde(default = "default::status")] pub status: u16, #[serde(default)] pub headers: BTreeMap, - #[serde(default)] - pub body: serde_json::Value, - #[serde(default)] - #[serde(skip_serializing_if = "Option::is_none")] - pub text_body: Option, + #[serde(flatten, default)] + pub body: Option, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +pub enum APIBody { + #[serde(rename = "textBody")] + Text(String), + #[serde(rename = "fileBody")] + File(String), + #[serde(rename = "body")] + Value(Value), } -fn default_status() -> u16 { - 200 +impl APIBody { + pub fn to_bytes(&self) -> Vec { + match self { + APIBody::Value(value) => serde_json::to_vec(value) + .unwrap_or_else(|_| core::panic!("Failed to convert value: {value:?}")), + APIBody::Text(text) => string_to_bytes(text), + APIBody::File(file) => { + let path: Vec<&str> = file.rsplitn(2, '/').collect(); + match &path[..] { + &[file, prefix] => match prefix { + "grpc/reflection" => { + let path = + Path::new(tailcall_fixtures::grpc::reflection::SELF).join(file); + std::fs::read(&path).unwrap_or_else(|_| { + core::panic!("Failed to read file by path: {}", path.display()) + }) + } + _ => core::panic!("Invalid file path: {} {}", prefix, file), + }, + _ => core::panic!("Invalid file path: {}", file), + } + } + } + } } -fn default_expected_hits() -> usize { - 1 +fn string_to_bytes(input: &str) -> Vec { + let mut bytes = Vec::new(); + let mut chars = input.chars().peekable(); + + while let Some(c) = chars.next() { + match c { + '\\' => match chars.next() { + Some('0') => bytes.push(0), + Some('n') => bytes.push(b'\n'), + Some('t') => bytes.push(b'\t'), + Some('r') => bytes.push(b'\r'), + Some('\\') => bytes.push(b'\\'), + Some('\"') => bytes.push(b'\"'), + Some('x') => { + let mut hex = chars.next().unwrap().to_string(); + hex.push(chars.next().unwrap()); + let byte = u8::from_str_radix(&hex, 16).unwrap(); + bytes.push(byte); + } + _ => panic!("Unsupported escape sequence"), + }, + _ => bytes.push(c as u8), + } + } + + bytes } diff --git a/tests/core/parse.rs b/tests/core/parse.rs index 6ab08d26c0..303744f300 100644 --- a/tests/core/parse.rs +++ b/tests/core/parse.rs @@ -109,7 +109,6 @@ impl ExecutionSpec { if let Some(Node::Text(text)) = heading.children.first() { runner = Some(match text.value.as_str() { "skip" => Annotation::Skip, - "only" => Annotation::Only, _ => { return Err(anyhow!( "Unexpected runner annotation {:?} in {:?}", diff --git a/tests/core/runtime.rs b/tests/core/runtime.rs index 1ccd61a6e1..0b198a4796 100644 --- a/tests/core/runtime.rs +++ b/tests/core/runtime.rs @@ -45,33 +45,16 @@ pub struct ExecutionMock { impl ExecutionMock { pub fn test_hits(&self, path: impl AsRef) { let url = &self.mock.request.0.url; - let is_batch_graphql = url.path().starts_with("/graphql") - && self - .mock - .request - .0 - .body - .as_str() - .map(|s| s.contains(',')) - .unwrap_or_default(); - - // do not test hits for mocks for batch graphql requests - // since that requires having 2 mocks with different order of queries in - // single request and only one of that mocks is actually called during run. - // for other protocols there is no issues right now, because: - // - for http the keys are always sorted https://github.com/tailcallhq/tailcall/blob/51d8b7aff838f0f4c362d4ee9e39492ae1f51fdb/src/http/data_loader.rs#L71 - // - for grpc body is not used for matching the mock and grpc will use grouping based on id https://github.com/tailcallhq/tailcall/blob/733b641c41f17c60b15b36b025b4db99d0f9cdcd/tests/execution_spec.rs#L769 - if is_batch_graphql { + if !self.mock.assert_hits { return; } - let expected_hits = self.mock.expected_hits; let actual_hits = self.actual_hits.load(Ordering::Relaxed); assert_eq!( expected_hits, actual_hits, - "expected mock for {url} to be hit exactly {expected_hits} times, but it was hit {actual_hits} times for file: {:?}", + "expected mock for {} to be hit exactly {} times, but it was hit {} times for file: {:?}", url, expected_hits, actual_hits, path.as_ref() ); } diff --git a/tests/core/snapshots/grpc-reflection.md_0.snap b/tests/core/snapshots/grpc-reflection.md_0.snap new file mode 100644 index 0000000000..f4f2990d59 --- /dev/null +++ b/tests/core/snapshots/grpc-reflection.md_0.snap @@ -0,0 +1,24 @@ +--- +source: tests/core/spec.rs +expression: response +--- +{ + "status": 200, + "headers": { + "content-type": "application/json" + }, + "body": { + "data": { + "news": { + "news": [ + { + "id": 1 + }, + { + "id": 2 + } + ] + } + } + } +} diff --git a/tests/core/snapshots/grpc-reflection.md_client.snap b/tests/core/snapshots/grpc-reflection.md_client.snap new file mode 100644 index 0000000000..66514b8486 --- /dev/null +++ b/tests/core/snapshots/grpc-reflection.md_client.snap @@ -0,0 +1,34 @@ +--- +source: tests/core/spec.rs +expression: client +--- +scalar Date + +scalar Email + +scalar Empty + +scalar JSON + +type News { + body: String + id: Int + postImage: String + title: String +} + +type NewsData { + news: [News]! +} + +scalar PhoneNumber + +type Query { + news: NewsData! +} + +scalar Url + +schema { + query: Query +} diff --git a/tests/core/snapshots/grpc-reflection.md_merged.snap b/tests/core/snapshots/grpc-reflection.md_merged.snap new file mode 100644 index 0000000000..ef18012e5e --- /dev/null +++ b/tests/core/snapshots/grpc-reflection.md_merged.snap @@ -0,0 +1,22 @@ +--- +source: tests/core/spec.rs +expression: merged +--- +schema @server(graphiql: true, port: 8000) @upstream(baseURL: "http://localhost:50051", httpCache: true) @link(src: "http://localhost:50051", type: Grpc) { + query: Query +} + +type News { + body: String + id: Int + postImage: String + title: String +} + +type NewsData { + news: [News]! +} + +type Query { + news: NewsData! @grpc(method: "news.NewsService.GetAllNews") +} diff --git a/tests/core/spec.rs b/tests/core/spec.rs index 1d8ff01960..6b3e5b72af 100644 --- a/tests/core/spec.rs +++ b/tests/core/spec.rs @@ -193,11 +193,12 @@ async fn run_query_tests_on_spec( let response: APIResponse = APIResponse { status: response.status().clone().as_u16(), headers, - body: serde_json::from_slice( - &hyper::body::to_bytes(response.into_body()).await.unwrap(), - ) - .unwrap_or(serde_json::Value::Null), - text_body: None, + body: Some(APIBody::Value( + serde_json::from_slice( + &hyper::body::to_bytes(response.into_body()).await.unwrap(), + ) + .unwrap_or_default(), + )), }; let snapshot_name = format!("{}_{}", spec.safe_name, i); @@ -289,7 +290,6 @@ pub async fn load_and_test_execution_spec(path: &Path) -> anyhow::Result<()> { Some(Annotation::Skip) => { println!("{} ... {}", spec.path.display(), "skipped".blue()); } - Some(Annotation::Only) => {} None => test_spec(spec).await, } @@ -300,7 +300,12 @@ async fn run_test( app_ctx: Arc, request: &APIRequest, ) -> anyhow::Result> { - let query_string = serde_json::to_string(&request.body).expect("body is required"); + let body = request + .body + .as_ref() + .map(|body| Body::from(body.to_bytes())) + .unwrap_or_default(); + let method = request.method.clone(); let headers = request.headers.clone(); let url = request.url.clone(); @@ -312,7 +317,7 @@ async fn run_test( .uri(url.as_str()), |acc, (key, value)| acc.header(key, value), ) - .body(Body::from(query_string))?; + .body(body)?; // TODO: reuse logic from server.rs to select the correct handler if app_ctx.blueprint.server.enable_batch_requests { diff --git a/tests/execution/add-field-index-list.md b/tests/execution/add-field-index-list.md index ac6dc19844..2b58267db9 100644 --- a/tests/execution/add-field-index-list.md +++ b/tests/execution/add-field-index-list.md @@ -18,7 +18,6 @@ type Query @addField(name: "username", path: ["users", "0", "name"]) { - request: method: GET url: http://jsonplaceholder.typicode.com/users - body: null response: status: 200 body: diff --git a/tests/execution/add-field-modify.md b/tests/execution/add-field-modify.md index d9a240a893..46549bd3b4 100644 --- a/tests/execution/add-field-modify.md +++ b/tests/execution/add-field-modify.md @@ -28,7 +28,6 @@ type Query { - request: method: GET url: http://jsonplaceholder.typicode.com/users/1 - body: null response: status: 200 body: diff --git a/tests/execution/add-field-with-composition.md b/tests/execution/add-field-with-composition.md index 341cda6a30..732bc40115 100644 --- a/tests/execution/add-field-with-composition.md +++ b/tests/execution/add-field-with-composition.md @@ -30,8 +30,7 @@ type Query - request: method: GET url: http://jsonplaceholder.typicode.com/users/1 - body: null - expected_hits: 2 + expectedHits: 2 response: status: 200 body: diff --git a/tests/execution/add-field-with-modify.md b/tests/execution/add-field-with-modify.md index 8e3d863e69..ec738b8f11 100644 --- a/tests/execution/add-field-with-modify.md +++ b/tests/execution/add-field-with-modify.md @@ -18,7 +18,6 @@ type Query @addField(name: "user1", path: ["person1", "name"]) @addField(name: " - request: method: GET url: http://jsonplaceholder.typicode.com/users/1 - body: null response: status: 200 body: @@ -27,7 +26,6 @@ type Query @addField(name: "user1", path: ["person1", "name"]) @addField(name: " - request: method: GET url: http://jsonplaceholder.typicode.com/users/2 - body: null response: status: 200 body: diff --git a/tests/execution/add-field.md b/tests/execution/add-field.md index 143af2042e..5fab2d7367 100644 --- a/tests/execution/add-field.md +++ b/tests/execution/add-field.md @@ -26,7 +26,6 @@ type Query { - request: method: GET url: http://jsonplaceholder.typicode.com/users/1 - body: null response: status: 200 body: diff --git a/tests/execution/apollo-tracing.md b/tests/execution/apollo-tracing.md index 97ff0f0a85..e534dc0780 100644 --- a/tests/execution/apollo-tracing.md +++ b/tests/execution/apollo-tracing.md @@ -16,7 +16,6 @@ type Query { - request: method: GET url: http://api.com - body: null response: status: 200 body: hello diff --git a/tests/execution/batching-default.md b/tests/execution/batching-default.md index d19cfe9c92..1c8936528a 100644 --- a/tests/execution/batching-default.md +++ b/tests/execution/batching-default.md @@ -32,7 +32,6 @@ type User { - request: method: GET url: http://jsonplaceholder.typicode.com/posts?id=11&id=3&foo=1 - body: null response: status: 200 body: @@ -43,7 +42,6 @@ type User { - request: method: GET url: http://jsonplaceholder.typicode.com/users?id=1&foo=bar&id=2&foo=bar - body: null response: status: 200 body: diff --git a/tests/execution/batching-disabled.md b/tests/execution/batching-disabled.md index 7d8e03c9eb..4c8d4c6ea0 100644 --- a/tests/execution/batching-disabled.md +++ b/tests/execution/batching-disabled.md @@ -59,7 +59,6 @@ - request: method: GET url: http://jsonplaceholder.typicode.com/users/1 - body: null response: status: 200 body: @@ -68,7 +67,6 @@ - request: method: GET url: http://jsonplaceholder.typicode.com/users/2 - body: null response: status: 200 body: diff --git a/tests/execution/batching-group-by-default.md b/tests/execution/batching-group-by-default.md index 738f736cf3..19b6e035a8 100644 --- a/tests/execution/batching-group-by-default.md +++ b/tests/execution/batching-group-by-default.md @@ -34,7 +34,6 @@ type User { - request: method: GET url: http://jsonplaceholder.typicode.com/posts?id=11&id=3&foo=1 - body: null response: status: 200 body: @@ -49,7 +48,6 @@ type User { - request: method: GET url: http://jsonplaceholder.typicode.com/users?id=1&foo=bar&id=2&foo=bar - body: null response: status: 200 body: diff --git a/tests/execution/batching-group-by.md b/tests/execution/batching-group-by.md index d5da78701f..e78ab62899 100644 --- a/tests/execution/batching-group-by.md +++ b/tests/execution/batching-group-by.md @@ -34,7 +34,6 @@ type User { - request: method: GET url: http://jsonplaceholder.typicode.com/posts?id=11&id=3&foo=1 - body: null response: status: 200 body: @@ -49,7 +48,6 @@ type User { - request: method: GET url: http://jsonplaceholder.typicode.com/users?id=1&foo=bar&id=2&foo=bar - body: null response: status: 200 body: diff --git a/tests/execution/batching-post.md b/tests/execution/batching-post.md index e298206f1b..a2a6d42c87 100644 --- a/tests/execution/batching-post.md +++ b/tests/execution/batching-post.md @@ -33,7 +33,6 @@ type User { - request: method: GET url: http://jsonplaceholder.typicode.com/posts?id=1 - body: null response: status: 200 body: @@ -42,7 +41,6 @@ type User { - request: method: GET url: http://jsonplaceholder.typicode.com/users/1 - body: null response: status: 200 body: diff --git a/tests/execution/batching.md b/tests/execution/batching.md index e47fc05479..ebe91eecb9 100644 --- a/tests/execution/batching.md +++ b/tests/execution/batching.md @@ -46,8 +46,7 @@ url: http://jsonplaceholder.typicode.com/users/1 headers: test: test - body: null - expected_hits: 2 + expectedHits: 2 response: status: 200 body: diff --git a/tests/execution/cache-control.md b/tests/execution/cache-control.md index efb6a3fd9e..ffef976562 100644 --- a/tests/execution/cache-control.md +++ b/tests/execution/cache-control.md @@ -59,8 +59,7 @@ url: http://jsonplaceholder.typicode.com/users?id=1 headers: test: test - body: null - expected_hits: 3 + expectedHits: 3 response: status: 200 headers: @@ -73,7 +72,6 @@ url: http://jsonplaceholder.typicode.com/users?id=2 headers: test: test - body: null response: status: 200 headers: @@ -86,8 +84,7 @@ url: http://jsonplaceholder.typicode.com/users?id=3 headers: test: test - body: null - expected_hits: 2 + expectedHits: 2 response: status: 200 headers: @@ -100,8 +97,7 @@ url: http://jsonplaceholder.typicode.com/users?id=4 headers: test: test - body: null - expected_hits: 2 + expectedHits: 2 response: status: 200 headers: diff --git a/tests/execution/caching-collision.md b/tests/execution/caching-collision.md index ede3c014fd..116a20828b 100644 --- a/tests/execution/caching-collision.md +++ b/tests/execution/caching-collision.md @@ -23,7 +23,6 @@ type Bar { - request: method: GET url: http://example.com/bars - body: null response: status: 200 body: @@ -130,7 +129,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorIxBZh4Jszyqh8SEjmXWIQmYUtglFnoImL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -138,7 +136,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=ByVLvrvaKTxZdgeFvbPbckXSorIxBUh4Jszyqh8SEjmXWIQmYUtglFnoImL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -146,7 +143,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorIxBUh4Jszyqh8SE3mXWIQmYUtglFnoImL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -154,7 +150,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BEVLvrvaKTxZdgeFvbPbckXSorIxBUh4Jszyqh8SEjmXWIQmYUtglFnoImL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -162,7 +157,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorIxBUh4Jszyqh8SEjmXWIQmYUtglFnoImL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DFPDXIwHe - body: null response: status: 200 body: @@ -170,7 +164,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSoYIxBUh4Jszyqh8SEjmXWIQmYUtglFnoImL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -178,7 +171,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZigeFvbPbckXSorIxBUh4Jszyqh8SEjmXWIQmYUtglFnoImL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -186,7 +178,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorIxBUh4Jszyqh8SEomXWIQmYUtglFnoImL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -194,7 +185,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKFxZdgeFvbPbckXSorIxBUh4Jszyqh8SEjmXWIQmYUtglFnoImL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -202,7 +192,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvePbckXSorIxBUh4Jszyqh8SEjmXWIQmYUtglFnoImL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -210,7 +199,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorIxBUh4Jsz1qh8SEjmXWIQmYUtglFnoImL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -218,7 +206,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorIxBUh4Jszyqh8SEjmXWIQmYUtglFnoImL33F5FYO25TXzQ5f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -226,7 +213,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorIxBUh4Jszyqh8SEjmXWIQmYftglFnoImL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -234,7 +220,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorIxBUh4Jszyqh8SEjmXWIQ7YUtglFnoImL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -242,7 +227,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorIxBUh4Jszyqp8SEjmXWIQmYUtglFnoImL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -250,7 +234,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorIxBUh4Jszyqh8SEjmXWIQmYUtglFnoImL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -258,7 +241,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorIxBUh4Jszyqh8SEjmXWIQmYUtHlFnoImL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -266,7 +248,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbczXSorIxBUh4Jszyqh8SEjmXWIQmYUtglFnoImL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -274,7 +255,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeF6bPbckXSorIxBUh4Jszyqh8SEjmXWIQmYUtglFnoImL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -282,7 +262,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFGbPbckXSorIxBUh4Jszyqh8SEjmXWIQmYUtglFnoImL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -290,7 +269,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSxrIxBUh4Jszyqh8SEjmXWIQmYUtglFnoImL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -298,7 +276,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSo9IxBUh4Jszyqh8SEjmXWIQmYUtglFnoImL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -306,7 +283,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorIxBUh4Jszyqh8SEjrXWIQmYUtglFnoImL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -314,7 +290,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorIxBUh4Jszyqh8SEjmXWIQmYUtglFnoImL33F5FtO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -322,7 +297,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorIxBUh4Jszyqh8SEjmXWIQmYUtglFnoImL33F5FYO25TXzQ3f7Zamx1sf3n2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -330,7 +304,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorIxBUh4Jszyqh8SEjmXWIQmYUtglFnoImL33F5FoO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -338,7 +311,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvVvaKTxZdgeFvbPbckXSorIxBUh4Jszyqh8SEjmXWIQmYUtglFnoImL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -346,7 +318,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorIxBUh4Jszyqh8SEjmXWIQmYUtglFnoImL33F5FYO25TXzQ3f7Zamx1sf3VpzFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -354,7 +325,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorIxBUh4Jszyqh8SEjmXWIQmYUtglFnoumL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -362,7 +332,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTYZdgeFvbPbckXSorIxBUh4Jszyqh8SEjmXWIQmYUtglFnoImL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -370,7 +339,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorIxBUh4Jszyqh8SEjmXWIQmYUtgRFnoImL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -378,7 +346,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorIxBUh4Jszyqh8SEjmXWIQmYUtglFnoImL33F5FYO25TXzQ3f7Zamx1sfZV2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -386,7 +353,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorIxBUh4Jszyqh8SEjmXWIQmYUtglFnoImL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwKe - body: null response: status: 200 body: @@ -394,7 +360,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorIxBUh4Jjzyqh8SEjmXWIQmYUtglFnoImL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -402,7 +367,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorIxBUh4Jszyqh8SEjmcWIQmYUtglFnoImL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -410,7 +374,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorIxBUh4Jszyqh8SEjmXWIQmYUtglFnoImL33F5FYO25TXzQ3f7Zamx1sf3V2zFw4i4DNPDXIwHe - body: null response: status: 200 body: @@ -418,7 +381,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorIxBUh4Jszyqh8SEjmXWIQmYUtglFnoImL3345FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -426,7 +388,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgaFvbPbckXSorIxBUh4Jszyqh8SEjmXWIQmYUtglFnoImL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -434,7 +395,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorIxBUh4Jszyqh8SEjmXWIQmYUtglFnoImR33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -442,7 +402,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXRorIxBUh4Jszyqh8SEjmXWIQmYUtglFnoImL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -450,7 +409,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvfKTxZdgeFvbPbckXSorIxBUh4Jszyqh8SEjmXWIQmYUtglFnoImL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -458,7 +416,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorIxBUh4Jszyqh8SEjmXWIQmYUtglFnoImL33F5FYO25TXzQIf7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -466,7 +423,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorIxBUh4Jszyqh8SEjmXWIQmYUtglFnOImL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -474,7 +430,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPmckXSorIxBUh4Jszyqh8SEjmXWIQmYUtglFnoImL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -482,7 +437,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbc9XSorIxBUh4Jszyqh8SEjmXWIQmYUtglFnoImL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -490,7 +444,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorIxBUh4Jszyqh8SEjmXWIQmYUtglFnoImL33F5FYO25TXzQ3q7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -498,7 +451,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFkbPbckXSorIxBUh4Jszyqh8SEjmXWIQmYUtglFnoImL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -506,7 +458,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorIxBUh4Jszyqh8SEjmXWIQmYUtglFnoTmL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -514,7 +465,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorIxBUh4Jszyqh8SEjmXWIFmYUtglFnoImL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -522,7 +472,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorIxBUh4Jszyqh8SEjmXWIQmYUtglFnoImL33d5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -530,7 +479,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSosIxBUh4Jszyqh8SEjmXWIQmYUtglFnoImL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -538,7 +486,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorIxBUh4Jszyqh8SVjmXWIQmYUtglFnoImL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -546,7 +493,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorIxBUh4JszyJh8SEjmXWIQmYUtglFnoImL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -554,7 +500,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorIxBUh4Jszyqh8SEjmXWIQmYUtglFhoImL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -562,7 +507,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSSrIxBUh4Jszyqh8SEjmXWIQmYUtglFnoImL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -570,7 +514,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorIxBUh4Jszyqh8SEjmXWIQmYUtglFnoImL33F5FYO25TXzQ3f7Za6x1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -578,7 +521,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFwbPbckXSorIxBUh4Jszyqh8SEjmXWIQmYUtglFnoImL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -586,7 +528,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorIxBUh4Jszyqh8SIjmXWIQmYUtglFnoImL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -594,7 +535,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorIxBUh4Jszyqh8SEjmXWIQmYUtglFnoImt33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -602,7 +542,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorIxBUh4Jszyqh8SEjmXWIQmYUtglFnoIwL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -610,7 +549,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorIxBUh4Jszyqh8SEjmXWIQmYUtglFnoImL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdw4DNPDXIwHe - body: null response: status: 200 body: @@ -618,7 +556,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorIxBUh4J6zyqh8SEjmXWIQmYUtglFnoImL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -626,7 +563,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorIxBUh4Jskyqh8SEjmXWIQmYUtglFnoImL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -634,7 +570,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorIxBUh4Jszyqh8SEjmXWIQmYUtglFnoImL33F5FYO25TXzQ3f7Zamx1sf3V2NFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -642,7 +577,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorFxBUh4Jszyqh8SEjmXWIQmYUtglFnoImL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -650,7 +584,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorIxBUh4Jszyqh8SEjmXWIQmYUtglFnoImL33F5FYO25yXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -658,7 +591,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorIxBUh4lszyqh8SEjmXWIQmYUtglFnoImL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -666,7 +598,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorIxBUh4Jszyqh8SEjmXWIQmYUtglFnoImL33F5FYO25TXzQ3f7Zamx1sf3V2zFmdi4DNPDXIwHe - body: null response: status: 200 body: @@ -674,7 +605,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorIxBUh4Jsjyqh8SEjmXWIQmYUtglFnoImL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -682,7 +612,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorIxBUh4Jszyqh8SEjmXWIQmYUtglFnoImL33F5FYO25TXzQ3f7Zamx1sfXV2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -690,7 +619,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorIxBUh4Jszyqh8SEjmXWIQmYUtglFnoImL33F5FYe25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -698,7 +626,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSogIxBUh4Jszyqh8SEjmXWIQmYUtglFnoImL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -706,7 +633,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorIxBUh4Jszyqh8SEjmXWIQmYUtglFnoImL33n5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -714,7 +640,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZqgeFvbPbckXSorIxBUh4Jszyqh8SEjmXWIQmYUtglFnoImL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -722,7 +647,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorIxBUh4Jszyqh8SEjmXWIQmYUtglFnoImL33F5FYO25TXzQ3f7Zamx1sf3V2zFwJi4DNPDXIwHe - body: null response: status: 200 body: @@ -730,7 +654,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorIxBUh4Jszyqh8SEjmXWIQmYUtglFnoKmL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -738,7 +661,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorIxBUh4Jszyqh8SEjmXWIQmYUtglFnoImL33F5FY625TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -746,7 +668,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTzZdgeFvbPbckXSorIxBUh4Jszyqh8SEjmXWIQmYUtglFnoImL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -754,7 +675,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorIxBUh4Jszyqh8SEjmXWIQmYUtglFnoIFL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -762,7 +682,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvNKTxZdgeFvbPbckXSorIxBUh4Jszyqh8SEjmXWIQmYUtglFnoImL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -770,7 +689,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTMZdgeFvbPbckXSorIxBUh4Jszyqh8SEjmXWIQmYUtglFnoImL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -778,7 +696,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorIxBUh4Jszyqh8SEjmXWIQmYUtglFyoImL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -786,7 +703,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorIxBUh4Jszyqh8SEjmXWIQmYUtglFnoImL33F5FYO25TXzQ3f7Zamx1sf3VqzFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -794,7 +710,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorIxBUh4oszyqh8SEjmXWIQmYUtglFnoImL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -802,7 +717,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorIxBUh4Jszyqh8SEjmXWIQmYUtglFnoImL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDHIwHe - body: null response: status: 200 body: @@ -810,7 +724,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKvxZdgeFvbPbckXSorIxBUh4Jszyqh8SEjmXWIQmYUtglFnoImL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -818,7 +731,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorIxBUh4Jszyqh8SEjmXWIQmYUtglFnoImL33F5FYO25TXzQ3f7Zamx1yf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -826,7 +738,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorIxBUh4Jszyqh8SEjmXWIQmYUtglFnoImL33F5FYO25TXzQ3f8Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -834,7 +745,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorIxBUh4Jszyqh8SEjmXWIQmYUtglFnoImL33F5FYO25rXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -842,7 +752,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgPFvbPbckXSorIxBUh4Jszyqh8SEjmXWIQmYUtglFnoImL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -850,7 +759,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorIxBUh4Jszyqh8SEjmXWIQmYUtglFnoImL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi45NPDXIwHe - body: null response: status: 200 body: @@ -858,7 +766,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorIxBUh4Jszyqh8SEjmXWIQmYUtglFnoImL33F5FYO25TXzQHf7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -866,7 +773,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPb9kXSorIxBUh4Jszyqh8SEjmXWIQmYUtglFnoImL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -874,7 +780,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorIxBUh4Jszyqh8SEjmXWIQmYUtglFnonmL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -882,7 +787,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorIxBUhkJszyqh8SEjmXWIQmYUtglFnoImL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -890,7 +794,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvFKTxZdgeFvbPbckXSorIxBUh4Jszyqh8SEjmXWIQmYUtglFnoImL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -898,7 +801,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorIxBUh4Jszyqh8SEjmXWIQmYUtglFnoImL33F5SYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -906,7 +808,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorIxBUh4Pszyqh8SEjmXWIQmYUtglFnoImL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -914,7 +815,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorIxBUh4Jszyqh8SEjmXWIQmYUtgHFnoImL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: @@ -922,7 +822,6 @@ type Bar { - request: method: GET url: http://example.com/foo?id=BVVLvrvaKTxZdgeFvbPbckXSorIxBUh4Jszyqh8SEjmXWIQmIUtglFnoImL33F5FYO25TXzQ3f7Zamx1sf3V2zFwdi4DNPDXIwHe - body: null response: status: 200 body: diff --git a/tests/execution/call-graphql-datasource.md b/tests/execution/call-graphql-datasource.md index bbe28c7dd8..df32c6d7ad 100644 --- a/tests/execution/call-graphql-datasource.md +++ b/tests/execution/call-graphql-datasource.md @@ -35,7 +35,6 @@ type Post { - request: method: GET url: http://jsonplaceholder.typicode.com/posts - body: null response: status: 200 body: @@ -54,8 +53,8 @@ type Post { - request: method: POST url: http://upstream/graphql - body: '{ "query": "query { user(id: 1) { name } }" }' - expected_hits: 1 + textBody: '{ "query": "query { user(id: 1) { name } }" }' + expectedHits: 1 response: status: 200 body: @@ -65,8 +64,8 @@ type Post { - request: method: POST url: http://upstream/graphql - body: '{ "query": "query { user(id: 2) { name } }" }' - expected_hits: 1 + textBody: '{ "query": "query { user(id: 2) { name } }" }' + expectedHits: 1 response: status: 200 body: diff --git a/tests/execution/call-mutation.md b/tests/execution/call-mutation.md index 38b1902190..929d6bbf8f 100644 --- a/tests/execution/call-mutation.md +++ b/tests/execution/call-mutation.md @@ -55,8 +55,8 @@ type User { - request: method: POST url: http://jsonplaceholder.typicode.com/posts - body: '{"body":"post-body","title":"post-title","userId":1}' - expected_hits: 2 + body: {"body": "post-body", "title": "post-title", "userId": 1} + expectedHits: 2 response: status: 200 body: @@ -66,7 +66,6 @@ type User { - request: method: GET url: http://jsonplaceholder.typicode.com/users/1 - body: null response: status: 200 body: @@ -75,7 +74,7 @@ type User { - request: method: PATCH url: http://jsonplaceholder.typicode.com/users/1 - body: '{"postId":1}' + body: {"postId": 1} response: status: 200 body: @@ -84,7 +83,6 @@ type User { - request: method: GET url: http://jsonplaceholder.typicode.com/posts?userId=1 - body: null response: status: 200 body: @@ -103,7 +101,7 @@ type User { - request: method: POST url: http://jsonplaceholder.typicode.com/users/1/posts - body: '{"body":"post-body","title":"post-title"}' + body: {"body": "post-body", "title": "post-title"} response: status: 200 body: diff --git a/tests/execution/call-operator.md b/tests/execution/call-operator.md index f674b5d632..cfff8e49ce 100644 --- a/tests/execution/call-operator.md +++ b/tests/execution/call-operator.md @@ -109,7 +109,7 @@ type Post { ```yml @mock - request: url: http://jsonplaceholder.typicode.com/users/1 - expected_hits: 4 + expectedHits: 4 response: body: id: 1 @@ -124,7 +124,7 @@ type Post { name: "Leanne Graham http headers" - request: url: http://jsonplaceholder.typicode.com/posts - expected_hits: 9 + expectedHits: 9 response: body: - id: 1 @@ -138,10 +138,10 @@ type Post { - request: url: http://upstream/graphql method: POST - body: '{ "query": "query { user { name } }" }' + textBody: '{ "query": "query { user { name } }" }' headers: id: 1 - expected_hits: 2 + expectedHits: 2 response: body: data: @@ -149,7 +149,7 @@ type Post { name: "Leanne Graham" - request: url: http://jsonplaceholder.typicode.com/posts?userId=1 - expected_hits: 2 + expectedHits: 2 response: body: - id: 1 @@ -163,9 +163,9 @@ type Post { - request: url: http://localhost:50051/news.NewsService/GetAllNews method: POST - expected_hits: 4 + expectedHits: 4 response: - body: \0\0\0\0t\n#\x08\x01\x12\x06Note 1\x1a\tContent 1\"\x0cPost image 1\n#\x08\x02\x12\x06Note 2\x1a\tContent 2\"\x0cPost image 2 + textBody: \0\0\0\0t\n#\x08\x01\x12\x06Note 1\x1a\tContent 1\"\x0cPost image 1\n#\x08\x02\x12\x06Note 2\x1a\tContent 2\"\x0cPost image 2 ``` ```yml @test diff --git a/tests/execution/env-value.md b/tests/execution/env-value.md index c34f6b8d4a..41b5d5541f 100644 --- a/tests/execution/env-value.md +++ b/tests/execution/env-value.md @@ -66,7 +66,6 @@ - request: method: GET url: http://jsonplaceholder.typicode.com/posts/1 - body: null response: status: 200 body: @@ -77,7 +76,6 @@ - request: method: GET url: http://jsonplaceholder.typicode.com/posts/2 - body: null response: status: 200 body: @@ -88,7 +86,6 @@ - request: method: GET url: http://jsonplaceholder.typicode.com/posts/3 - body: null response: status: 200 body: diff --git a/tests/execution/experimental-headers.md b/tests/execution/experimental-headers.md index f2c730e5e5..35689aad50 100644 --- a/tests/execution/experimental-headers.md +++ b/tests/execution/experimental-headers.md @@ -19,7 +19,6 @@ type User { - request: method: GET url: http://jsonplaceholder.typicode.com/users - body: null response: status: 200 headers: diff --git a/tests/execution/graphql-dataloader-batch-keys.md b/tests/execution/graphql-dataloader-batch-keys.md index fd98c7e993..c0b065f99c 100644 --- a/tests/execution/graphql-dataloader-batch-keys.md +++ b/tests/execution/graphql-dataloader-batch-keys.md @@ -39,7 +39,7 @@ type B { - request: method: POST url: http://upstream/graphql - body: '[{"query": "a {id, bid, cid}"}]' + textBody: '[{"query": "a {id, bid, cid}"}]' response: status: 200 body: @@ -60,7 +60,7 @@ type B { - request: method: POST url: http://upstream/graphql - body: '[{"query": "b {y}"},{"query": "c {x}"}]' + textBody: '[{"query": "b {y}"},{"query": "c {x}"}]' response: status: 200 body: @@ -73,7 +73,7 @@ type B { - request: method: POST url: http://upstream/graphql - body: '[{"query": "c {x}"},{"query": "b {y}"}]' + textBody: '[{"query": "c {x}"},{"query": "b {y}"}]' response: status: 200 body: diff --git a/tests/execution/graphql-dataloader-batch-request.md b/tests/execution/graphql-dataloader-batch-request.md index 11b224dee2..f81f742a83 100644 --- a/tests/execution/graphql-dataloader-batch-request.md +++ b/tests/execution/graphql-dataloader-batch-request.md @@ -32,7 +32,6 @@ type Query { - request: method: GET url: http://jsonplaceholder.typicode.com/posts - body: null response: status: 200 body: @@ -51,7 +50,8 @@ type Query { - request: method: POST url: http://upstream/graphql - body: '[{ "query": "query { user(id: 1) { name } }" },{ "query": "query { user(id: 2) { name } }" }]' + textBody: '[{ "query": "query { user(id: 1) { name } }" },{ "query": "query { user(id: 2) { name } }" }]' + assertHits: false response: status: 200 body: @@ -64,7 +64,8 @@ type Query { - request: method: POST url: http://upstream/graphql - body: '[{ "query": "query { user(id: 2) { name } }" },{ "query": "query { user(id: 1) { name } }" }]' + textBody: '[{ "query": "query { user(id: 2) { name } }" },{ "query": "query { user(id: 1) { name } }" }]' + assertHits: false response: status: 200 body: diff --git a/tests/execution/graphql-dataloader-no-batch-request.md b/tests/execution/graphql-dataloader-no-batch-request.md index abe425285a..585eb4da92 100644 --- a/tests/execution/graphql-dataloader-no-batch-request.md +++ b/tests/execution/graphql-dataloader-no-batch-request.md @@ -26,7 +26,6 @@ type Query { - request: method: GET url: http://jsonplaceholder.typicode.com/posts - body: null response: status: 200 body: @@ -45,7 +44,7 @@ type Query { - request: method: POST url: http://upstream/graphql - body: '{ "query": "query { user(id: 1) { name } }" }' + textBody: '{ "query": "query { user(id: 1) { name } }" }' response: status: 200 body: @@ -55,7 +54,7 @@ type Query { - request: method: POST url: http://upstream/graphql - body: '{ "query": "query { user(id: 2) { name } }" }' + textBody: '{ "query": "query { user(id: 2) { name } }" }' response: status: 200 body: diff --git a/tests/execution/graphql-datasource-errors.md b/tests/execution/graphql-datasource-errors.md index e643247a2e..788fd4de94 100644 --- a/tests/execution/graphql-datasource-errors.md +++ b/tests/execution/graphql-datasource-errors.md @@ -20,7 +20,7 @@ type Query { - request: method: POST url: http://upstream/graphql - body: '{ "query": "query { user(id: 1) { name } }" }' + textBody: '{ "query": "query { user(id: 1) { name } }" }' response: status: 200 body: @@ -35,7 +35,7 @@ type Query { - request: method: POST url: http://upstream/graphql - body: '{ "query": "query { user(id: 2) { name id } }" }' + textBody: '{ "query": "query { user(id: 2) { name id } }" }' response: status: 200 body: diff --git a/tests/execution/graphql-datasource-mutation.md b/tests/execution/graphql-datasource-mutation.md index 5f23315987..52b9bda7ed 100644 --- a/tests/execution/graphql-datasource-mutation.md +++ b/tests/execution/graphql-datasource-mutation.md @@ -31,7 +31,7 @@ type UserInput { - request: method: POST url: http://upstream/graphql - body: '{ "query": "mutation { createUser(user: {name: \"Test Name\", email: \"test@email\"}) { name } }" }' + textBody: {"query": 'mutation { createUser(user: {name: "Test Name", email: "test@email"}) { name } }'} response: status: 200 body: diff --git a/tests/execution/graphql-datasource-no-args.md b/tests/execution/graphql-datasource-no-args.md index 36e05d8d45..3864a9ca3a 100644 --- a/tests/execution/graphql-datasource-no-args.md +++ b/tests/execution/graphql-datasource-no-args.md @@ -19,7 +19,7 @@ type Query { - request: method: POST url: http://upstream/graphql - body: '{ "query": "query { users { name } }" }' + textBody: {"query": "query { users { name } }"} response: status: 200 body: diff --git a/tests/execution/graphql-datasource-with-args.md b/tests/execution/graphql-datasource-with-args.md index 76aaa8766b..bc60e3ac45 100644 --- a/tests/execution/graphql-datasource-with-args.md +++ b/tests/execution/graphql-datasource-with-args.md @@ -27,7 +27,7 @@ type Query { - request: method: POST url: http://upstream/graphql - body: '{ "query": "query { user(id: 1) { name } }" }' + textBody: '{ "query": "query { user(id: 1) { name } }" }' response: status: 200 body: @@ -37,7 +37,7 @@ type Query { - request: method: POST url: http://upstream/graphql - body: '{ "query": "query { post(id: 1) { id user { name } } }" }' + textBody: '{ "query": "query { post(id: 1) { id user { name } } }" }' response: status: 200 body: diff --git a/tests/execution/grpc-batch.md b/tests/execution/grpc-batch.md index 2539e8df26..61072baec6 100644 --- a/tests/execution/grpc-batch.md +++ b/tests/execution/grpc-batch.md @@ -76,10 +76,19 @@ type News { - request: method: POST url: http://localhost:50051/news.NewsService/GetMultipleNews - body: \0\0\0\0\n\x02\x08\x02\n\x02\x08\x03 + textBody: \0\0\0\0\x08\n\x02\x08\x02\n\x02\x08\x03 + assertHits: false response: status: 200 - body: \0\0\0\0t\n#\x08\x02\x12\x06Note 2\x1a\tContent 2\"\x0cPost image 2\n#\x08\x03\x12\x06Note 3\x1a\tContent 3\"\x0cPost image 3 + textBody: \0\0\0\0t\n#\x08\x02\x12\x06Note 2\x1a\tContent 2\"\x0cPost image 2\n#\x08\x03\x12\x06Note 3\x1a\tContent 3\"\x0cPost image 3 +- request: + method: POST + url: http://localhost:50051/news.NewsService/GetMultipleNews + textBody: \0\0\0\0\x08\n\x02\x08\x03\n\x02\x08\x02 + assertHits: false + response: + status: 200 + textBody: \0\0\0\0t\n#\x08\x03\x12\x06Note 3\x1a\tContent 3\"\x0cPost image 3\n#\x08\x02\x12\x06Note 2\x1a\tContent 2\"\x0cPost image 2 ``` ```yml @test diff --git a/tests/execution/grpc-error.md b/tests/execution/grpc-error.md index 296e9530ed..2d0ebe2661 100644 --- a/tests/execution/grpc-error.md +++ b/tests/execution/grpc-error.md @@ -71,7 +71,6 @@ type News { - request: method: POST url: http://localhost:50051/news.NewsService/GetAllNews - body: null response: status: 200 headers: diff --git a/tests/execution/grpc-override-url-from-upstream.md b/tests/execution/grpc-override-url-from-upstream.md index 7e55dae589..718d5a04de 100644 --- a/tests/execution/grpc-override-url-from-upstream.md +++ b/tests/execution/grpc-override-url-from-upstream.md @@ -71,10 +71,9 @@ type News { - request: method: POST url: http://localhost:50051/news.NewsService/GetAllNews - body: null response: status: 200 - body: \0\0\0\0t\n#\x08\x01\x12\x06Note 1\x1a\tContent 1\"\x0cPost image 1\n#\x08\x02\x12\x06Note 2\x1a\tContent 2\"\x0cPost image 2 + textBody: \0\0\0\0t\n#\x08\x01\x12\x06Note 1\x1a\tContent 1\"\x0cPost image 1\n#\x08\x02\x12\x06Note 2\x1a\tContent 2\"\x0cPost image 2 ``` ```yml @test diff --git a/tests/execution/grpc-proto-with-same-package.md b/tests/execution/grpc-proto-with-same-package.md index b78549322a..67925aa28e 100644 --- a/tests/execution/grpc-proto-with-same-package.md +++ b/tests/execution/grpc-proto-with-same-package.md @@ -60,18 +60,16 @@ type Bar { - request: method: POST url: http://localhost:50051/test.FooService/GetFoo - body: null response: status: 200 - body: \0\0\0\0\n\n\x08test-foo + textBody: \0\0\0\0\n\n\x08test-foo - request: method: POST url: http://localhost:50051/test.BarService/GetBar - body: null response: status: 200 - body: \0\0\0\0\n\n\x08test-bar + textBody: \0\0\0\0\n\n\x08test-bar ``` ```yml @test diff --git a/tests/execution/grpc-reflection.md b/tests/execution/grpc-reflection.md new file mode 100644 index 0000000000..d054bd8399 --- /dev/null +++ b/tests/execution/grpc-reflection.md @@ -0,0 +1,57 @@ +# Grpc datasource + +```graphql @server +schema + @server(port: 8000, graphiql: true) + @upstream(httpCache: true, baseURL: "http://localhost:50051") + @link(src: "http://localhost:50051", type: Grpc) { + query: Query +} + +type Query { + news: NewsData! @grpc(method: "news.NewsService.GetAllNews") +} + +type NewsData { + news: [News]! +} + +type News { + id: Int + title: String + body: String + postImage: String +} +``` + +```yml @mock +- request: + method: POST + url: http://localhost:50051/grpc.reflection.v1alpha.ServerReflection/ServerReflectionInfo + textBody: \0\0\0\0\x02:\0 + response: + status: 200 + fileBody: grpc/reflection/news-list-services.bin + +- request: + method: POST + url: http://localhost:50051/grpc.reflection.v1alpha.ServerReflection/ServerReflectionInfo + textBody: \0\0\0\0\x12\"\x10news.NewsService + response: + status: 200 + fileBody: grpc/reflection/news-service-descriptor.bin + +- request: + method: POST + url: http://localhost:50051/news.NewsService/GetAllNews + response: + status: 200 + textBody: \0\0\0\0t\n#\x08\x01\x12\x06Note 1\x1a\tContent 1\"\x0cPost image 1\n#\x08\x02\x12\x06Note 2\x1a\tContent 2\"\x0cPost image 2 +``` + +```yml @test +- method: POST + url: http://localhost:8080/graphql + body: + query: query { news {news{ id }} } +``` diff --git a/tests/execution/grpc-simple.md b/tests/execution/grpc-simple.md index 5fbef81655..c38344903e 100644 --- a/tests/execution/grpc-simple.md +++ b/tests/execution/grpc-simple.md @@ -71,10 +71,9 @@ type News { - request: method: POST url: http://localhost:50051/news.NewsService/GetAllNews - body: null response: status: 200 - body: \0\0\0\0t\n#\x08\x01\x12\x06Note 1\x1a\tContent 1\"\x0cPost image 1\n#\x08\x02\x12\x06Note 2\x1a\tContent 2\"\x0cPost image 2 + textBody: \0\0\0\0t\n#\x08\x01\x12\x06Note 1\x1a\tContent 1\"\x0cPost image 1\n#\x08\x02\x12\x06Note 2\x1a\tContent 2\"\x0cPost image 2 ``` ```yml @test diff --git a/tests/execution/grpc-url-from-upstream.md b/tests/execution/grpc-url-from-upstream.md index e87029e111..db2c8ac4af 100644 --- a/tests/execution/grpc-url-from-upstream.md +++ b/tests/execution/grpc-url-from-upstream.md @@ -70,10 +70,9 @@ type News { - request: method: POST url: http://localhost:50051/news.NewsService/GetAllNews - body: null response: status: 200 - body: \0\0\0\0t\n#\x08\x01\x12\x06Note 1\x1a\tContent 1\"\x0cPost image 1\n#\x08\x02\x12\x06Note 2\x1a\tContent 2\"\x0cPost image 2 + textBody: \0\0\0\0t\n#\x08\x01\x12\x06Note 1\x1a\tContent 1\"\x0cPost image 1\n#\x08\x02\x12\x06Note 2\x1a\tContent 2\"\x0cPost image 2 ``` ```yml @test diff --git a/tests/execution/https.md b/tests/execution/https.md index 6ecc527098..0cd1a62a0a 100644 --- a/tests/execution/https.md +++ b/tests/execution/https.md @@ -44,7 +44,6 @@ - request: method: GET url: https://jsonplaceholder.typicode.com/users/1 - body: null response: status: 200 body: diff --git a/tests/execution/inline-field.md b/tests/execution/inline-field.md index 5ce02d1dee..638ffabff4 100644 --- a/tests/execution/inline-field.md +++ b/tests/execution/inline-field.md @@ -26,7 +26,6 @@ type Query { - request: method: GET url: http://jsonplaceholder.typicode.com/users/1 - body: null response: status: 200 body: diff --git a/tests/execution/inline-index-list.md b/tests/execution/inline-index-list.md index 338b6e01ab..071555e9c2 100644 --- a/tests/execution/inline-index-list.md +++ b/tests/execution/inline-index-list.md @@ -18,7 +18,6 @@ type Query @addField(name: "username", path: ["username", "0", "name"]) { - request: method: GET url: http://jsonplaceholder.typicode.com/users - body: null response: status: 200 body: diff --git a/tests/execution/io-cache.md b/tests/execution/io-cache.md index a4906aabb1..16f3161e15 100644 --- a/tests/execution/io-cache.md +++ b/tests/execution/io-cache.md @@ -29,7 +29,6 @@ type Post { - request: method: GET url: http://jsonplaceholder.typicode.com/posts - body: null response: status: 200 body: @@ -53,14 +52,14 @@ type Post { userId: 2 - request: url: http://jsonplaceholder.typicode.com/users/1 - expected_hits: 1 + expectedHits: 1 response: status: 200 body: name: Leanne Graham - request: url: http://jsonplaceholder.typicode.com/users/2 - expected_hits: 1 + expectedHits: 1 response: status: 200 body: diff --git a/tests/execution/jsonplaceholder-call-post.md b/tests/execution/jsonplaceholder-call-post.md index 9348c93593..0b4674de28 100644 --- a/tests/execution/jsonplaceholder-call-post.md +++ b/tests/execution/jsonplaceholder-call-post.md @@ -29,7 +29,7 @@ type Post { ```yml @mock - request: url: http://jsonplaceholder.typicode.com/posts - expected_hits: 1 + expectedHits: 1 response: body: - id: 1 @@ -37,7 +37,7 @@ type Post { userId: 1 - request: url: http://jsonplaceholder.typicode.com/users/1 - expected_hits: 1 + expectedHits: 1 response: body: id: 1 diff --git a/tests/execution/modified-field.md b/tests/execution/modified-field.md index 3514b15ba3..ffca3bf021 100644 --- a/tests/execution/modified-field.md +++ b/tests/execution/modified-field.md @@ -18,7 +18,6 @@ type Query { - request: method: GET url: http://jsonplaceholder.typicode.com/users/1 - body: null response: status: 200 body: diff --git a/tests/execution/mutation-put.md b/tests/execution/mutation-put.md index 3dd6f6eb31..e411bdfaf6 100644 --- a/tests/execution/mutation-put.md +++ b/tests/execution/mutation-put.md @@ -38,7 +38,7 @@ type User { - request: method: PUT url: http://jsonplaceholder.typicode.com/posts/100 - body: '{"body":"abc","id":100,"title":"bar","userId":1}' + body: {"body": "abc", "id": 100, "title": "bar", "userId": 1} response: status: 200 body: diff --git a/tests/execution/mutation.md b/tests/execution/mutation.md index 277dc6e225..8f0c4399a1 100644 --- a/tests/execution/mutation.md +++ b/tests/execution/mutation.md @@ -37,7 +37,7 @@ type User { - request: method: POST url: http://jsonplaceholder.typicode.com/posts - body: '{"body":"post-body","title":"post-title","userId":1}' + body: {"body": "post-body", "title": "post-title", "userId": 1} response: status: 200 body: diff --git a/tests/execution/n-plus-one-list.md b/tests/execution/n-plus-one-list.md index 50c8be80cd..b17ae0237f 100644 --- a/tests/execution/n-plus-one-list.md +++ b/tests/execution/n-plus-one-list.md @@ -27,7 +27,6 @@ type Bar { - request: method: GET url: http://example.com/bars - body: null response: status: 200 body: @@ -42,7 +41,6 @@ type Bar { - request: method: GET url: http://example.com/foos?id=1&id=2 - body: null response: status: 200 body: diff --git a/tests/execution/n-plus-one.md b/tests/execution/n-plus-one.md index da96ec58c3..e6649fa0af 100644 --- a/tests/execution/n-plus-one.md +++ b/tests/execution/n-plus-one.md @@ -27,7 +27,6 @@ type Bar { - request: method: GET url: http://example.com/foos - body: null response: status: 200 body: @@ -38,7 +37,6 @@ type Bar { - request: method: GET url: http://example.com/bars?fooId=1&fooId=2 - body: null response: status: 200 body: diff --git a/tests/execution/nested-objects.md b/tests/execution/nested-objects.md index 88280a0c98..20c6357125 100644 --- a/tests/execution/nested-objects.md +++ b/tests/execution/nested-objects.md @@ -28,7 +28,6 @@ type Query { - request: method: GET url: http://jsonplaceholder.typicode.com/users/1 - body: null response: status: 200 body: diff --git a/tests/execution/nesting-level3.md b/tests/execution/nesting-level3.md index 5356278ca9..393662e6f0 100644 --- a/tests/execution/nesting-level3.md +++ b/tests/execution/nesting-level3.md @@ -35,7 +35,6 @@ type Post { - request: method: GET url: http://jsonplaceholder.typicode.com/posts/1 - body: null response: status: 200 body: @@ -43,7 +42,6 @@ type Post { - request: method: GET url: http://jsonplaceholder.typicode.com/users/1 - body: null response: status: 200 body: @@ -52,7 +50,6 @@ type Post { - request: method: GET url: http://jsonplaceholder.typicode.com/users/1/todos - body: null response: status: 200 body: diff --git a/tests/execution/nullable-arg-query.md b/tests/execution/nullable-arg-query.md index 0b6d06c1d2..b0dce76c45 100644 --- a/tests/execution/nullable-arg-query.md +++ b/tests/execution/nullable-arg-query.md @@ -20,7 +20,6 @@ type User { - request: method: GET url: http://jsonplaceholder.typicode.com/users - body: null response: status: 200 body: @@ -37,7 +36,6 @@ type User { - request: method: GET url: http://jsonplaceholder.typicode.com/users?id=1 - body: null response: status: 200 body: diff --git a/tests/execution/omit-index-list.md b/tests/execution/omit-index-list.md index 338b6e01ab..071555e9c2 100644 --- a/tests/execution/omit-index-list.md +++ b/tests/execution/omit-index-list.md @@ -18,7 +18,6 @@ type Query @addField(name: "username", path: ["username", "0", "name"]) { - request: method: GET url: http://jsonplaceholder.typicode.com/users - body: null response: status: 200 body: diff --git a/tests/execution/omit-resolved-by-parent.md b/tests/execution/omit-resolved-by-parent.md index 690fd33151..31c225be45 100644 --- a/tests/execution/omit-resolved-by-parent.md +++ b/tests/execution/omit-resolved-by-parent.md @@ -22,7 +22,6 @@ type User @addField(name: "address", path: ["address", "street"]) { - request: method: GET url: http://jsonplaceholder.typicode.com/users/1 - body: null response: status: 200 body: diff --git a/tests/execution/recursive-type-json.md b/tests/execution/recursive-type-json.md index 2655349b84..56848de283 100644 --- a/tests/execution/recursive-type-json.md +++ b/tests/execution/recursive-type-json.md @@ -53,7 +53,6 @@ - request: method: GET url: https://jsonplaceholder.typicode.com/users/1 - body: null response: status: 200 body: @@ -62,7 +61,6 @@ - request: method: GET url: https://jsonplaceholder.typicode.com/friends/1 - body: null response: status: 200 body: diff --git a/tests/execution/recursive-types.md b/tests/execution/recursive-types.md index 9195f64644..ed21992989 100644 --- a/tests/execution/recursive-types.md +++ b/tests/execution/recursive-types.md @@ -20,7 +20,6 @@ type Query { - request: method: GET url: https://jsonplaceholder.typicode.com/users/1 - body: null response: status: 200 body: @@ -29,7 +28,6 @@ type Query { - request: method: GET url: https://jsonplaceholder.typicode.com/friends/1 - body: null response: status: 200 body: diff --git a/tests/execution/ref-other-nested.md b/tests/execution/ref-other-nested.md index 15b6078488..7b104a3e3b 100644 --- a/tests/execution/ref-other-nested.md +++ b/tests/execution/ref-other-nested.md @@ -66,8 +66,7 @@ - request: method: GET url: https://jsonplaceholder.typicode.com/users/1 - body: null - expected_hits: 1 + expectedHits: 1 response: status: 200 body: diff --git a/tests/execution/ref-other.md b/tests/execution/ref-other.md index cf80ec91e8..c38521b510 100644 --- a/tests/execution/ref-other.md +++ b/tests/execution/ref-other.md @@ -23,7 +23,6 @@ type Query { - request: method: GET url: https://jsonplaceholder.typicode.com/users/1 - body: null response: status: 200 body: diff --git a/tests/execution/rename-field.md b/tests/execution/rename-field.md index 60cb6c145a..7d9aa53414 100644 --- a/tests/execution/rename-field.md +++ b/tests/execution/rename-field.md @@ -18,7 +18,6 @@ type Query { - request: method: GET url: http://jsonplaceholder.typicode.com/users/1 - body: null response: status: 200 body: @@ -27,7 +26,6 @@ type Query { - request: method: GET url: http://jsonplaceholder.typicode.com/users/2 - body: null response: status: 200 body: diff --git a/tests/execution/request-to-upstream-batching.md b/tests/execution/request-to-upstream-batching.md index eaf6e253c8..eb97ccce75 100644 --- a/tests/execution/request-to-upstream-batching.md +++ b/tests/execution/request-to-upstream-batching.md @@ -65,7 +65,6 @@ url: http://jsonplaceholder.typicode.com/users?id=1&id=2 headers: test: test - body: null response: status: 200 body: diff --git a/tests/execution/resolve-with-vars.md b/tests/execution/resolve-with-vars.md index 1fa40c396a..2303654204 100644 --- a/tests/execution/resolve-with-vars.md +++ b/tests/execution/resolve-with-vars.md @@ -20,7 +20,6 @@ type Query { - request: method: GET url: http://jsonplaceholder.typicode.com/users?id=1 - body: null response: status: 200 body: diff --git a/tests/execution/resolved-by-parent.md b/tests/execution/resolved-by-parent.md index 690fd33151..31c225be45 100644 --- a/tests/execution/resolved-by-parent.md +++ b/tests/execution/resolved-by-parent.md @@ -22,7 +22,6 @@ type User @addField(name: "address", path: ["address", "street"]) { - request: method: GET url: http://jsonplaceholder.typicode.com/users/1 - body: null response: status: 200 body: diff --git a/tests/execution/rest-api-error.md b/tests/execution/rest-api-error.md index 39640447c4..a498049199 100644 --- a/tests/execution/rest-api-error.md +++ b/tests/execution/rest-api-error.md @@ -31,7 +31,6 @@ type User { - request: method: GET url: http://jsonplaceholder.typicode.com/users/1 - body: null response: status: 200 body: {} diff --git a/tests/execution/rest-api-post.md b/tests/execution/rest-api-post.md index 987c759bf9..3a154e5cb2 100644 --- a/tests/execution/rest-api-post.md +++ b/tests/execution/rest-api-post.md @@ -33,7 +33,6 @@ type User { url: http://jsonplaceholder.typicode.com/users/1 headers: test: test - body: null response: status: 200 body: diff --git a/tests/execution/rest-api.md b/tests/execution/rest-api.md index 05a1cfbeda..08301d8364 100644 --- a/tests/execution/rest-api.md +++ b/tests/execution/rest-api.md @@ -33,7 +33,6 @@ type User { url: http://jsonplaceholder.typicode.com/users/1 headers: test: test - body: null response: status: 200 body: diff --git a/tests/execution/showcase.md b/tests/execution/showcase.md index b8ff81dc53..44f2f48546 100644 --- a/tests/execution/showcase.md +++ b/tests/execution/showcase.md @@ -21,7 +21,6 @@ type Query { url: http://jsonplaceholder.typicode.com/users/1 headers: test: test - body: null response: status: 200 body: @@ -30,8 +29,7 @@ type Query { - request: method: GET url: http://example.com/simple.graphql - body: null - expected_hits: 2 + expectedHits: 2 response: status: 200 textBody: |2- @@ -41,7 +39,6 @@ type Query { - request: method: GET url: http://example.com/invalid.graphql - body: null response: status: 200 body: dsjfsjdfjdsfjkdskjfjkds diff --git a/tests/execution/simple-graphql.md b/tests/execution/simple-graphql.md index 0dafcf3d2d..250dbf7a82 100644 --- a/tests/execution/simple-graphql.md +++ b/tests/execution/simple-graphql.md @@ -21,7 +21,6 @@ type Query { url: http://jsonplaceholder.typicode.com/users/1 headers: test: test - body: null response: status: 200 body: diff --git a/tests/execution/simple-query.md b/tests/execution/simple-query.md index 14ef977730..e803311ff7 100644 --- a/tests/execution/simple-query.md +++ b/tests/execution/simple-query.md @@ -43,7 +43,6 @@ - request: method: GET url: http://jsonplaceholder.typicode.com/users/1 - body: null response: status: 200 body: diff --git a/tests/execution/test-enum-default.md b/tests/execution/test-enum-default.md index b7af57e5cd..37c163fc68 100644 --- a/tests/execution/test-enum-default.md +++ b/tests/execution/test-enum-default.md @@ -65,10 +65,10 @@ type NewsData { - request: method: POST url: http://localhost:50051/news.NewsService/GetAllNews - body: '\0\0\0\0\0' + textBody: \0\0\0\0\0 response: status: 200 - body: '\0\0\0\0s\n#\x08\x01\x12\x06Note 1\x1a\tContent 1\"\x0cPost image 1\n%\x08\x02\x12\x06Note 2\x1a\tContent 2\"\x0cPost image 2(\x01\n%\x08\x03\x12\x06Note 3\x1a\tContent 3\"\x0cPost image 3(\x02' + textBody: '\0\0\0\0s\n#\x08\x01\x12\x06Note 1\x1a\tContent 1\"\x0cPost image 1\n%\x08\x02\x12\x06Note 2\x1a\tContent 2\"\x0cPost image 2(\x01\n%\x08\x03\x12\x06Note 3\x1a\tContent 3\"\x0cPost image 3(\x02' ``` ```yml @test diff --git a/tests/execution/test-params-as-body.md b/tests/execution/test-params-as-body.md index a1f75f0acc..f9d2c346b3 100644 --- a/tests/execution/test-params-as-body.md +++ b/tests/execution/test-params-as-body.md @@ -19,7 +19,7 @@ type User { - request: method: POST url: http://jsonplaceholder.typicode.com/users - body: '{"id":1,"name":"foo"}' + body: {"id": 1, "name": "foo"} response: status: 200 body: diff --git a/tests/execution/test-static-value.md b/tests/execution/test-static-value.md index 63d17357f2..a36d245d1e 100644 --- a/tests/execution/test-static-value.md +++ b/tests/execution/test-static-value.md @@ -42,7 +42,6 @@ - request: method: GET url: http://jsonplaceholder.typicode.com/users/1 - body: null response: status: 200 body: diff --git a/tests/execution/test-upstream-headers.md b/tests/execution/test-upstream-headers.md index fe2f643565..2c57085d70 100644 --- a/tests/execution/test-upstream-headers.md +++ b/tests/execution/test-upstream-headers.md @@ -19,7 +19,6 @@ type Post { headers: x-foo: bar x-bar: baz - body: null response: status: 200 body: diff --git a/tests/execution/upstream-batching.md b/tests/execution/upstream-batching.md index 3d4741c7b6..b5a782fafa 100644 --- a/tests/execution/upstream-batching.md +++ b/tests/execution/upstream-batching.md @@ -62,7 +62,6 @@ url: http://jsonplaceholder.typicode.com/users?id=1&id=2 headers: test: test - body: null response: status: 200 body: diff --git a/tests/execution/upstream-fail-request.md b/tests/execution/upstream-fail-request.md index 886ac9d923..fa3e1bc1be 100644 --- a/tests/execution/upstream-fail-request.md +++ b/tests/execution/upstream-fail-request.md @@ -19,7 +19,6 @@ type Query { - request: method: GET url: http://jsonplaceholder.typicode.com/users/1 - body: null response: status: 503 body: {} diff --git a/tests/execution/with-args-url.md b/tests/execution/with-args-url.md index 242b886c91..0ad9853fc3 100644 --- a/tests/execution/with-args-url.md +++ b/tests/execution/with-args-url.md @@ -52,7 +52,6 @@ url: http://jsonplaceholder.typicode.com/users/1 headers: test: test - body: null response: status: 200 body: diff --git a/tests/execution/with-args.md b/tests/execution/with-args.md index 512383cb87..06cd07b479 100644 --- a/tests/execution/with-args.md +++ b/tests/execution/with-args.md @@ -19,7 +19,6 @@ type Query { - request: method: GET url: http://jsonplaceholder.typicode.com/users?id=1 - body: null response: status: 200 body: diff --git a/tests/execution/with-nesting.md b/tests/execution/with-nesting.md index 5b0ef08034..120d3674d5 100644 --- a/tests/execution/with-nesting.md +++ b/tests/execution/with-nesting.md @@ -31,7 +31,6 @@ type Post { - request: method: GET url: http://jsonplaceholder.typicode.com/users/1 - body: null response: status: 200 body: @@ -42,7 +41,6 @@ type Post { - request: method: GET url: http://jsonplaceholder.typicode.com/users/1/posts - body: null response: status: 200 body: