From dc817d46f0bb2df7c03a558df4b6dfdbea3dc03f Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Mon, 16 Sep 2024 18:23:18 +0200 Subject: [PATCH 1/6] defer integration tests --- apollo-router/tests/common.rs | 1 - .../tests/samples/basic/defer/README.md | 3 + .../samples/basic/defer/configuration.yaml | 4 + .../tests/samples/basic/defer/plan.json | 53 ++++++++ .../samples/basic/defer/supergraph.graphql | 125 ++++++++++++++++++ apollo-router/tests/samples_tests.rs | 7 +- 6 files changed, 191 insertions(+), 2 deletions(-) create mode 100644 apollo-router/tests/samples/basic/defer/README.md create mode 100644 apollo-router/tests/samples/basic/defer/configuration.yaml create mode 100644 apollo-router/tests/samples/basic/defer/plan.json create mode 100644 apollo-router/tests/samples/basic/defer/supergraph.graphql diff --git a/apollo-router/tests/common.rs b/apollo-router/tests/common.rs index 42eb5d3c9a..5f6c7e5fc3 100644 --- a/apollo-router/tests/common.rs +++ b/apollo-router/tests/common.rs @@ -582,7 +582,6 @@ impl IntegrationTest { let mut request = builder.json(&query).build().unwrap(); telemetry.inject_context(&mut request); - request.headers_mut().remove(ACCEPT); match client.execute(request).await { Ok(response) => (span_id, response), Err(err) => { diff --git a/apollo-router/tests/samples/basic/defer/README.md b/apollo-router/tests/samples/basic/defer/README.md new file mode 100644 index 0000000000..9386489fb0 --- /dev/null +++ b/apollo-router/tests/samples/basic/defer/README.md @@ -0,0 +1,3 @@ +This is an example test + +This file adds some context that will be displayed on test failure \ No newline at end of file diff --git a/apollo-router/tests/samples/basic/defer/configuration.yaml b/apollo-router/tests/samples/basic/defer/configuration.yaml new file mode 100644 index 0000000000..f7ed04641e --- /dev/null +++ b/apollo-router/tests/samples/basic/defer/configuration.yaml @@ -0,0 +1,4 @@ +override_subgraph_url: + products: http://localhost:4005 +include_subgraph_errors: + all: true diff --git a/apollo-router/tests/samples/basic/defer/plan.json b/apollo-router/tests/samples/basic/defer/plan.json new file mode 100644 index 0000000000..0bb0370008 --- /dev/null +++ b/apollo-router/tests/samples/basic/defer/plan.json @@ -0,0 +1,53 @@ +{ + "actions": [ + { + "type": "Start", + "schema_path": "./supergraph.graphql", + "configuration_path": "./configuration.yaml", + "subgraphs": { + "accounts": { + "requests": [ + { + "request": { + "body": { + "query": "{me{name}}" + } + }, + "response": { + "body": { + "data": { + "me": { + "name": "test" + } + } + } + } + } + ] + }, + "reviews": { + "requests": [] + } + } + }, + { + "type": "Request", + "headers": { + "Accept": "multipart/mixed;deferSpec=20220824" + }, + "request": { + "query": "{ me { name ... @defer { reviews { body } } } }" + }, + "expected_response": { + "data": { + "me": { + "name": "Ada Lovelace" + } + } + } + }, + { + "type": "Stop" + } + ] +} \ No newline at end of file diff --git a/apollo-router/tests/samples/basic/defer/supergraph.graphql b/apollo-router/tests/samples/basic/defer/supergraph.graphql new file mode 100644 index 0000000000..1bd9f596ee --- /dev/null +++ b/apollo-router/tests/samples/basic/defer/supergraph.graphql @@ -0,0 +1,125 @@ +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/inaccessible/v0.2", for: SECURITY) + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { + query: Query + mutation: Mutation +} + +directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION + +directive @tag( + name: String! +) repeatable on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION | SCHEMA + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements( + graph: join__Graph! + interface: String! +) repeatable on OBJECT | INTERFACE + +directive @join__type( + graph: join__Graph! + key: join__FieldSet + extension: Boolean! = false + resolvable: Boolean! = true + isInterfaceObject: Boolean! = false +) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember( + graph: join__Graph! + member: String! +) repeatable on UNION + +directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA + +scalar join__FieldSet + +enum join__Graph { + ACCOUNTS + @join__graph(name: "accounts", url: "https://accounts.demo.starstuff.dev/") + INVENTORY + @join__graph( + name: "inventory" + url: "https://inventory.demo.starstuff.dev/" + ) + PRODUCTS + @join__graph(name: "products", url: "https://products.demo.starstuff.dev/") + REVIEWS + @join__graph(name: "reviews", url: "https://reviews.demo.starstuff.dev/") +} + +scalar link__Import + +enum link__Purpose { + SECURITY + EXECUTION +} + +type Mutation @join__type(graph: PRODUCTS) @join__type(graph: REVIEWS) { + createProduct(upc: ID!, name: String): Product @join__field(graph: PRODUCTS) + createReview(upc: ID!, id: ID!, body: String): Review + @join__field(graph: REVIEWS) +} + +type Product + @join__type(graph: INVENTORY, key: "upc") + @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: REVIEWS, key: "upc") { + inStock: Boolean + @join__field(graph: INVENTORY) + @tag(name: "private") + @inaccessible + name: String @join__field(graph: PRODUCTS) + weight: Int @join__field(graph: INVENTORY, external: true) @join__field(graph: PRODUCTS) + price: Int @join__field(graph: INVENTORY, external: true) @join__field(graph: PRODUCTS) + reviews: [Review] @join__field(graph: REVIEWS) + reviewsForAuthor(authorID: ID!): [Review] @join__field(graph: REVIEWS) + shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") + upc: String! +} + +type Query + @join__type(graph: ACCOUNTS) + @join__type(graph: INVENTORY) + @join__type(graph: PRODUCTS) + @join__type(graph: REVIEWS) { + me: User @join__field(graph: ACCOUNTS) + topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS) +} + +type Review @join__type(graph: REVIEWS, key: "id") { + id: ID! + body: String @join__field(graph: REVIEWS) + author: User @join__field(graph: REVIEWS, provides: "username") + product: Product @join__field(graph: REVIEWS) +} + +type User + @join__type(graph: ACCOUNTS, key: "id") + @join__type(graph: REVIEWS, key: "id") { + id: ID! + name: String @join__field(graph: ACCOUNTS) + username: String + @join__field(graph: ACCOUNTS) + @join__field(graph: REVIEWS, external: true) + reviews: [Review] @join__field(graph: REVIEWS) +} diff --git a/apollo-router/tests/samples_tests.rs b/apollo-router/tests/samples_tests.rs index e3fd0d5264..2e88b2546c 100644 --- a/apollo-router/tests/samples_tests.rs +++ b/apollo-router/tests/samples_tests.rs @@ -446,7 +446,12 @@ impl TestExecution { f })?; let graphql_response: Value = serde_json::from_slice(&body).map_err(|e| { - writeln!(out, "could not deserialize graphql response data: {e}").unwrap(); + writeln!( + out, + "could not deserialize graphql response data: {e}\nfrom:\n{}", + std::str::from_utf8(&body).unwrap() + ) + .unwrap(); let f: Failed = out.clone().into(); f })?; From b40fc22c674eae7be25417ec22ec8263eccbb7cd Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Mon, 16 Sep 2024 19:02:21 +0200 Subject: [PATCH 2/6] try to parse the multipart body --- apollo-router/tests/samples_tests.rs | 100 +++++++++++++++++++++++---- 1 file changed, 87 insertions(+), 13 deletions(-) diff --git a/apollo-router/tests/samples_tests.rs b/apollo-router/tests/samples_tests.rs index 2e88b2546c..6d857d2e35 100644 --- a/apollo-router/tests/samples_tests.rs +++ b/apollo-router/tests/samples_tests.rs @@ -14,6 +14,9 @@ use std::process::ExitCode; use libtest_mimic::Arguments; use libtest_mimic::Failed; use libtest_mimic::Trial; +use mediatype::MediaTypeList; +use mediatype::ReadParams; +use multer::Multipart; use serde::Deserialize; use serde_json::Value; use tokio::runtime::Runtime; @@ -437,24 +440,95 @@ impl TestExecution { } writeln!(out, "query: {}\n", serde_json::to_string(&request).unwrap()).unwrap(); + writeln!(out, "header: {:?}\n", headers).unwrap(); + let (_, response) = router .execute_query_with_headers(&request, headers.clone()) .await; - let body = response.bytes().await.map_err(|e| { - writeln!(out, "could not get graphql response data: {e}").unwrap(); - let f: Failed = out.clone().into(); - f - })?; - let graphql_response: Value = serde_json::from_slice(&body).map_err(|e| { - writeln!( + writeln!(out, "response headers: {:?}", response.headers()).unwrap(); + + let content_type = response + .headers() + .get("content-type") + .unwrap() + .to_str() + .unwrap(); + let mut is_multipart = false; + let mut boundary = None; + for result in MediaTypeList::new(content_type) { + if let Ok(mime) = result { + if mime.ty == mediatype::names::MULTIPART && mime.subty == mediatype::names::MIXED { + is_multipart = true; + boundary = mime + .get_param(mediatype::names::BOUNDARY) + .map(|v| v.as_str().to_string()); + } + } + } + + let graphql_response: Value = if !is_multipart { + let body = response.bytes().await.map_err(|e| { + writeln!(out, "could not get graphql response data: {e}").unwrap(); + let f: Failed = out.clone().into(); + f + })?; + serde_json::from_slice(&body).map_err(|e| { + writeln!( + out, + "could not deserialize graphql response data: {e}\nfrom:\n{}", + std::str::from_utf8(&body).unwrap() + ) + .unwrap(); + let f: Failed = out.clone().into(); + f + })? + } else { + writeln!(out, "is_multipart, boundary={boundary:?}").unwrap(); + /*writeln!( out, - "could not deserialize graphql response data: {e}\nfrom:\n{}", - std::str::from_utf8(&body).unwrap() + "entire response:\n{}", + std::str::from_utf8(&response.bytes().await.unwrap()).unwrap() ) - .unwrap(); - let f: Failed = out.clone().into(); - f - })?; + .unwrap();*/ + + let mut chunks = Vec::new(); + + let mut multipart = Multipart::new(response.bytes_stream(), boundary.unwrap()); + + // Iterate over the fields, use `next_field()` to get the next field. + while let Some(mut field) = multipart.next_field().await.map_err(|e| { + writeln!(out, "could not get next field from multipart body: {e}",).unwrap(); + let f: Failed = out.clone().into(); + f + })? { + writeln!(out, "multipart field: {:?}\n", field).unwrap(); + + let name = field.name(); + // Get the field's filename if provided in "Content-Disposition" header. + let file_name = field.file_name(); + + while let Some(chunk) = field.chunk().await.map_err(|e| { + writeln!(out, "could not get next chunk from multipart body: {e}",).unwrap(); + let f: Failed = out.clone().into(); + f + })? { + writeln!(out, "multipart chunk: {:?}\n", std::str::from_utf8(&chunk)).unwrap(); + + let parsed: Value = serde_json::from_slice(&chunk).map_err(|e| { + writeln!( + out, + "could not deserialize graphql response data: {e}\nfrom:\n{}", + std::str::from_utf8(&chunk).unwrap() + ) + .unwrap(); + let f: Failed = out.clone().into(); + f + })?; + chunks.push(parsed); + } + } + Value::Array(chunks) + }; if expected_response != &graphql_response { if let Some(requests) = self From edcb456801a4719a5187c4c1a956b128089c2d8f Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Tue, 17 Sep 2024 10:21:31 +0200 Subject: [PATCH 3/6] make the test pass --- .../tests/samples/basic/defer/plan.json | 71 ++++++++++++++++--- apollo-router/tests/samples_tests.rs | 26 +++---- 2 files changed, 71 insertions(+), 26 deletions(-) diff --git a/apollo-router/tests/samples/basic/defer/plan.json b/apollo-router/tests/samples/basic/defer/plan.json index 0bb0370008..b2499400bd 100644 --- a/apollo-router/tests/samples/basic/defer/plan.json +++ b/apollo-router/tests/samples/basic/defer/plan.json @@ -10,14 +10,16 @@ { "request": { "body": { - "query": "{me{name}}" + "query": "{me{__typename name id}}" } }, "response": { "body": { "data": { "me": { - "name": "test" + "__typename": "User", + "name": "test", + "id": "1" } } } @@ -26,7 +28,38 @@ ] }, "reviews": { - "requests": [] + "requests": [ + { + "request": { + "body": { + "query": "query($representations:[_Any!]!){_entities(representations:$representations){...on User{reviews{body}}}}", + "variables": { + "representations": [ + { + "__typename": "User", + "id": "1" + } + ] + } + } + }, + "response": { + "body": { + "data": { + "_entities": [ + { + "reviews": [ + { + "body": "Test" + } + ] + } + ] + } + } + } + } + ] } } }, @@ -38,13 +71,33 @@ "request": { "query": "{ me { name ... @defer { reviews { body } } } }" }, - "expected_response": { - "data": { - "me": { - "name": "Ada Lovelace" - } + "expected_response": [ + { + "data": { + "me": { + "name": "test" + } + }, + "hasNext": true + }, + { + "hasNext": false, + "incremental": [ + { + "data": { + "reviews": [ + { + "body": "Test" + } + ] + }, + "path": [ + "me" + ] + } + ] } - } + ] }, { "type": "Stop" diff --git a/apollo-router/tests/samples_tests.rs b/apollo-router/tests/samples_tests.rs index 6d857d2e35..3ae5a58ec5 100644 --- a/apollo-router/tests/samples_tests.rs +++ b/apollo-router/tests/samples_tests.rs @@ -459,9 +459,15 @@ impl TestExecution { if let Ok(mime) = result { if mime.ty == mediatype::names::MULTIPART && mime.subty == mediatype::names::MIXED { is_multipart = true; - boundary = mime - .get_param(mediatype::names::BOUNDARY) - .map(|v| v.as_str().to_string()); + boundary = mime.get_param(mediatype::names::BOUNDARY).map(|v| { + // multer does not strip quotes from the boundary: https://github.com/rwf2/multer/issues/64 + let mut s = v.as_str(); + if s.starts_with("\"") && s.ends_with("\"") { + s = &s[1..s.len() - 1]; + } + + s.to_string() + }); } } } @@ -483,14 +489,6 @@ impl TestExecution { f })? } else { - writeln!(out, "is_multipart, boundary={boundary:?}").unwrap(); - /*writeln!( - out, - "entire response:\n{}", - std::str::from_utf8(&response.bytes().await.unwrap()).unwrap() - ) - .unwrap();*/ - let mut chunks = Vec::new(); let mut multipart = Multipart::new(response.bytes_stream(), boundary.unwrap()); @@ -501,12 +499,6 @@ impl TestExecution { let f: Failed = out.clone().into(); f })? { - writeln!(out, "multipart field: {:?}\n", field).unwrap(); - - let name = field.name(); - // Get the field's filename if provided in "Content-Disposition" header. - let file_name = field.file_name(); - while let Some(chunk) = field.chunk().await.map_err(|e| { writeln!(out, "could not get next chunk from multipart body: {e}",).unwrap(); let f: Failed = out.clone().into(); From 7b4a1910ec22670bfdb0233e9282e0118d935df4 Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Tue, 17 Sep 2024 10:29:53 +0200 Subject: [PATCH 4/6] lint --- apollo-router/tests/samples_tests.rs | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/apollo-router/tests/samples_tests.rs b/apollo-router/tests/samples_tests.rs index 3ae5a58ec5..4e1c81383f 100644 --- a/apollo-router/tests/samples_tests.rs +++ b/apollo-router/tests/samples_tests.rs @@ -455,20 +455,18 @@ impl TestExecution { .unwrap(); let mut is_multipart = false; let mut boundary = None; - for result in MediaTypeList::new(content_type) { - if let Ok(mime) = result { - if mime.ty == mediatype::names::MULTIPART && mime.subty == mediatype::names::MIXED { - is_multipart = true; - boundary = mime.get_param(mediatype::names::BOUNDARY).map(|v| { - // multer does not strip quotes from the boundary: https://github.com/rwf2/multer/issues/64 - let mut s = v.as_str(); - if s.starts_with("\"") && s.ends_with("\"") { - s = &s[1..s.len() - 1]; - } - - s.to_string() - }); - } + for mime in MediaTypeList::new(content_type).flatten() { + if mime.ty == mediatype::names::MULTIPART && mime.subty == mediatype::names::MIXED { + is_multipart = true; + boundary = mime.get_param(mediatype::names::BOUNDARY).map(|v| { + // multer does not strip quotes from the boundary: https://github.com/rwf2/multer/issues/64 + let mut s = v.as_str(); + if s.starts_with('\"') && s.ends_with('\"') { + s = &s[1..s.len() - 1]; + } + + s.to_string() + }); } } From 57208f6447a06a23fa311db1f93a95b6d0177937 Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Tue, 17 Sep 2024 10:32:00 +0200 Subject: [PATCH 5/6] rename basic to core --- apollo-router/tests/samples/{basic => core}/defer/README.md | 0 .../tests/samples/{basic => core}/defer/configuration.yaml | 0 apollo-router/tests/samples/{basic => core}/defer/plan.json | 0 .../tests/samples/{basic => core}/defer/supergraph.graphql | 0 apollo-router/tests/samples/{basic => core}/query1/README.md | 0 .../tests/samples/{basic => core}/query1/configuration.yaml | 0 apollo-router/tests/samples/{basic => core}/query1/plan.json | 0 .../tests/samples/{basic => core}/query1/supergraph.graphql | 0 apollo-router/tests/samples/{basic => core}/query2/README.md | 0 .../tests/samples/{basic => core}/query2/configuration.yaml | 0 apollo-router/tests/samples/{basic => core}/query2/plan.json | 0 .../tests/samples/{basic => core}/query2/supergraph.graphql | 0 12 files changed, 0 insertions(+), 0 deletions(-) rename apollo-router/tests/samples/{basic => core}/defer/README.md (100%) rename apollo-router/tests/samples/{basic => core}/defer/configuration.yaml (100%) rename apollo-router/tests/samples/{basic => core}/defer/plan.json (100%) rename apollo-router/tests/samples/{basic => core}/defer/supergraph.graphql (100%) rename apollo-router/tests/samples/{basic => core}/query1/README.md (100%) rename apollo-router/tests/samples/{basic => core}/query1/configuration.yaml (100%) rename apollo-router/tests/samples/{basic => core}/query1/plan.json (100%) rename apollo-router/tests/samples/{basic => core}/query1/supergraph.graphql (100%) rename apollo-router/tests/samples/{basic => core}/query2/README.md (100%) rename apollo-router/tests/samples/{basic => core}/query2/configuration.yaml (100%) rename apollo-router/tests/samples/{basic => core}/query2/plan.json (100%) rename apollo-router/tests/samples/{basic => core}/query2/supergraph.graphql (100%) diff --git a/apollo-router/tests/samples/basic/defer/README.md b/apollo-router/tests/samples/core/defer/README.md similarity index 100% rename from apollo-router/tests/samples/basic/defer/README.md rename to apollo-router/tests/samples/core/defer/README.md diff --git a/apollo-router/tests/samples/basic/defer/configuration.yaml b/apollo-router/tests/samples/core/defer/configuration.yaml similarity index 100% rename from apollo-router/tests/samples/basic/defer/configuration.yaml rename to apollo-router/tests/samples/core/defer/configuration.yaml diff --git a/apollo-router/tests/samples/basic/defer/plan.json b/apollo-router/tests/samples/core/defer/plan.json similarity index 100% rename from apollo-router/tests/samples/basic/defer/plan.json rename to apollo-router/tests/samples/core/defer/plan.json diff --git a/apollo-router/tests/samples/basic/defer/supergraph.graphql b/apollo-router/tests/samples/core/defer/supergraph.graphql similarity index 100% rename from apollo-router/tests/samples/basic/defer/supergraph.graphql rename to apollo-router/tests/samples/core/defer/supergraph.graphql diff --git a/apollo-router/tests/samples/basic/query1/README.md b/apollo-router/tests/samples/core/query1/README.md similarity index 100% rename from apollo-router/tests/samples/basic/query1/README.md rename to apollo-router/tests/samples/core/query1/README.md diff --git a/apollo-router/tests/samples/basic/query1/configuration.yaml b/apollo-router/tests/samples/core/query1/configuration.yaml similarity index 100% rename from apollo-router/tests/samples/basic/query1/configuration.yaml rename to apollo-router/tests/samples/core/query1/configuration.yaml diff --git a/apollo-router/tests/samples/basic/query1/plan.json b/apollo-router/tests/samples/core/query1/plan.json similarity index 100% rename from apollo-router/tests/samples/basic/query1/plan.json rename to apollo-router/tests/samples/core/query1/plan.json diff --git a/apollo-router/tests/samples/basic/query1/supergraph.graphql b/apollo-router/tests/samples/core/query1/supergraph.graphql similarity index 100% rename from apollo-router/tests/samples/basic/query1/supergraph.graphql rename to apollo-router/tests/samples/core/query1/supergraph.graphql diff --git a/apollo-router/tests/samples/basic/query2/README.md b/apollo-router/tests/samples/core/query2/README.md similarity index 100% rename from apollo-router/tests/samples/basic/query2/README.md rename to apollo-router/tests/samples/core/query2/README.md diff --git a/apollo-router/tests/samples/basic/query2/configuration.yaml b/apollo-router/tests/samples/core/query2/configuration.yaml similarity index 100% rename from apollo-router/tests/samples/basic/query2/configuration.yaml rename to apollo-router/tests/samples/core/query2/configuration.yaml diff --git a/apollo-router/tests/samples/basic/query2/plan.json b/apollo-router/tests/samples/core/query2/plan.json similarity index 100% rename from apollo-router/tests/samples/basic/query2/plan.json rename to apollo-router/tests/samples/core/query2/plan.json diff --git a/apollo-router/tests/samples/basic/query2/supergraph.graphql b/apollo-router/tests/samples/core/query2/supergraph.graphql similarity index 100% rename from apollo-router/tests/samples/basic/query2/supergraph.graphql rename to apollo-router/tests/samples/core/query2/supergraph.graphql From 7a3d45a3dc97cfda0579409425661936a4c71605 Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Tue, 17 Sep 2024 16:12:05 +0200 Subject: [PATCH 6/6] test defer with the entity cache --- .../enterprise/entity-cache/defer/README.md | 3 + .../entity-cache/defer/configuration.yaml | 23 ++++ .../enterprise/entity-cache/defer/plan.json | 113 ++++++++++++++++ .../entity-cache/defer/supergraph.graphql | 122 ++++++++++++++++++ apollo-router/tests/samples_tests.rs | 27 ++++ 5 files changed, 288 insertions(+) create mode 100644 apollo-router/tests/samples/enterprise/entity-cache/defer/README.md create mode 100644 apollo-router/tests/samples/enterprise/entity-cache/defer/configuration.yaml create mode 100644 apollo-router/tests/samples/enterprise/entity-cache/defer/plan.json create mode 100644 apollo-router/tests/samples/enterprise/entity-cache/defer/supergraph.graphql diff --git a/apollo-router/tests/samples/enterprise/entity-cache/defer/README.md b/apollo-router/tests/samples/enterprise/entity-cache/defer/README.md new file mode 100644 index 0000000000..a96d350b73 --- /dev/null +++ b/apollo-router/tests/samples/enterprise/entity-cache/defer/README.md @@ -0,0 +1,3 @@ +# Entity cache with @defer + +This tests `Cache-Control` aggregation when using the `@defer` directive. \ No newline at end of file diff --git a/apollo-router/tests/samples/enterprise/entity-cache/defer/configuration.yaml b/apollo-router/tests/samples/enterprise/entity-cache/defer/configuration.yaml new file mode 100644 index 0000000000..fb6b95ecd4 --- /dev/null +++ b/apollo-router/tests/samples/enterprise/entity-cache/defer/configuration.yaml @@ -0,0 +1,23 @@ +override_subgraph_url: + products: http://localhost:4005 +include_subgraph_errors: + all: true + +preview_entity_cache: + enabled: true + redis: + urls: + ["redis://localhost:6379",] + subgraph: + all: + enabled: true + subgraphs: + reviews: + ttl: 120s + enabled: true + +telemetry: + exporters: + logging: + stdout: + format: text \ No newline at end of file diff --git a/apollo-router/tests/samples/enterprise/entity-cache/defer/plan.json b/apollo-router/tests/samples/enterprise/entity-cache/defer/plan.json new file mode 100644 index 0000000000..265e282056 --- /dev/null +++ b/apollo-router/tests/samples/enterprise/entity-cache/defer/plan.json @@ -0,0 +1,113 @@ +{ + "enterprise": true, + "redis": true, + "actions": [ + { + "type": "Start", + "schema_path": "./supergraph.graphql", + "configuration_path": "./configuration.yaml", + "subgraphs": { + "cache-defer-accounts": { + "requests": [ + { + "request": { + "body": { + "query": "query CacheDefer__cache_defer_accounts__0{me{__typename name id}}", + "operationName": "CacheDefer__cache_defer_accounts__0" + } + }, + "response": { + "headers": { + "Cache-Control": "public, max-age=10", + "Content-Type": "application/json" + }, + "body": { + "data": { + "me": { + "__typename": "User", + "name": "test-user", + "id": "1" + } + } + } + } + } + ] + }, + "cache-defer-reviews": { + "requests": [ + { + "request": { + "body": { + "query": "query CacheDefer__cache_defer_reviews__1($representations:[_Any!]!){_entities(representations:$representations){...on User{reviews{body}}}}", + "operationName": "CacheDefer__cache_defer_reviews__1", + "variables": { + "representations": [ + { + "id": "1", + "__typename": "User" + } + ] + } + } + }, + "response": { + "headers": { + "Cache-Control": "public, max-age=100", + "Content-Type": "application/json" + }, + "body": { + "data": { + "reviews": [ + { + "body": "test-review" + } + ] + } + } + } + } + ] + } + } + }, + { + "type": "Request", + "request": { + "query": "query CacheDefer { me { name ... @defer { reviews { body } } } }" + }, + "headers": { + "Accept": "multipart/mixed;deferSpec=20220824" + }, + "expected_response": [ + { + "data": { + "me": { + "name": "test-user" + } + }, + "hasNext": true + }, + { + "hasNext": false, + "incremental": [ + { + "data": { + "reviews": null + }, + "path": [ + "me" + ] + } + ] + } + ], + "expected_headers": { + "Cache-Control": "max-age=10,public" + } + }, + { + "type": "Stop" + } + ] +} \ No newline at end of file diff --git a/apollo-router/tests/samples/enterprise/entity-cache/defer/supergraph.graphql b/apollo-router/tests/samples/enterprise/entity-cache/defer/supergraph.graphql new file mode 100644 index 0000000000..320a9c2a70 --- /dev/null +++ b/apollo-router/tests/samples/enterprise/entity-cache/defer/supergraph.graphql @@ -0,0 +1,122 @@ +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/tag/v0.3") + @link(url: "https://specs.apollo.dev/inaccessible/v0.2") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { + query: Query + mutation: Mutation +} + +directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION +directive @tag( + name: String! +) repeatable on FIELD_DEFINITION | INTERFACE | OBJECT | UNION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements( + graph: join__Graph! + interface: String! +) repeatable on OBJECT | INTERFACE + +directive @join__type( + graph: join__Graph! + key: join__FieldSet + extension: Boolean! = false + resolvable: Boolean! = true + isInterfaceObject: Boolean! = false +) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember( + graph: join__Graph! + member: String! +) repeatable on UNION + +directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA + +scalar join__FieldSet +scalar link__Import + +enum join__Graph { + ACCOUNTS @join__graph(name: "cache-defer-accounts", url: "https://accounts.demo.starstuff.dev") + INVENTORY @join__graph(name: "inventory", url: "https://inventory.demo.starstuff.dev") + PRODUCTS @join__graph(name: "products", url: "https://products.demo.starstuff.dev") + REVIEWS @join__graph(name: "cache-defer-reviews", url: "https://reviews.demo.starstuff.dev") +} + +enum link__Purpose { + SECURITY + EXECUTION +} + +type Mutation + @join__type(graph: PRODUCTS) + @join__type(graph: REVIEWS) + @join__type(graph: ACCOUNTS) { + updateMyAccount: User @join__field(graph: ACCOUNTS) + createProduct(name: String, upc: ID!): Product @join__field(graph: PRODUCTS) + createReview(body: String, id: ID!, upc: ID!): Review + @join__field(graph: REVIEWS) +} + +type Product + @join__type(graph: ACCOUNTS, key: "upc", extension: true) + @join__type(graph: INVENTORY, key: "upc") + @join__type(graph: PRODUCTS, key: "upc") + @join__type(graph: REVIEWS, key: "upc") { + inStock: Boolean + @join__field(graph: INVENTORY) + @tag(name: "private") + @inaccessible + name: String @join__field(graph: PRODUCTS) + weight: Int @join__field(graph: INVENTORY, external: true) @join__field(graph: PRODUCTS) + price: Int @join__field(graph: INVENTORY, external: true) @join__field(graph: PRODUCTS) + reviews: [Review] @join__field(graph: REVIEWS) + reviewsForAuthor(authorID: ID!): [Review] @join__field(graph: REVIEWS) + shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") + upc: String! +} + +type Query + @join__type(graph: ACCOUNTS) + @join__type(graph: INVENTORY) + @join__type(graph: PRODUCTS) + @join__type(graph: REVIEWS) { + me: User @join__field(graph: ACCOUNTS) + topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS) +} + +type Review @join__type(graph: REVIEWS, key: "id") { + id: ID! + author: User @join__field(graph: REVIEWS, provides: "username") + body: String + product: Product +} + +type User + @join__type(graph: ACCOUNTS, key: "id") + @join__type(graph: REVIEWS, key: "id") { + id: ID! + name: String @join__field(graph: ACCOUNTS) + username: String + @join__field(graph: ACCOUNTS) + @join__field(graph: REVIEWS, external: true) + reviews: [Review] @join__field(graph: REVIEWS) +} diff --git a/apollo-router/tests/samples_tests.rs b/apollo-router/tests/samples_tests.rs index 4e1c81383f..1222a5652c 100644 --- a/apollo-router/tests/samples_tests.rs +++ b/apollo-router/tests/samples_tests.rs @@ -170,12 +170,14 @@ impl TestExecution { query_path, headers, expected_response, + expected_headers, } => { self.request( request.clone(), query_path.as_deref(), headers, expected_response, + expected_headers, path, out, ) @@ -417,6 +419,7 @@ impl TestExecution { query_path: Option<&str>, headers: &HashMap, expected_response: &Value, + expected_headers: &HashMap, path: &Path, out: &mut String, ) -> Result<(), Failed> { @@ -447,6 +450,28 @@ impl TestExecution { .await; writeln!(out, "response headers: {:?}", response.headers()).unwrap(); + let mut failed = false; + for (key, value) in expected_headers { + if !response.headers().contains_key(key) { + failed = true; + writeln!(out, "expected header {} to be present", key).unwrap(); + } else if response.headers().get(key).unwrap() != value { + failed = true; + writeln!( + out, + "expected header {} to be {}, got {:?}", + key, + value, + response.headers().get(key).unwrap() + ) + .unwrap(); + } + } + if failed { + let f: Failed = out.clone().into(); + return Err(f); + } + let content_type = response .headers() .get("content-type") @@ -652,6 +677,8 @@ enum Action { #[serde(default)] headers: HashMap, expected_response: Value, + #[serde(default)] + expected_headers: HashMap, }, EndpointRequest { url: url::Url,