From 6ba2548a2a1129b51af9ef8f309bd1d9ce951516 Mon Sep 17 00:00:00 2001 From: Lenny Burdette Date: Tue, 30 Jul 2024 11:56:33 -0500 Subject: [PATCH] test the quickstart example --- .../connectors/testdata/quickstart.graphql | 81 +++++ .../connectors/testdata/quickstart.yaml | 77 +++++ apollo-router/src/plugins/connectors/tests.rs | 281 ++++++++++++++++++ 3 files changed, 439 insertions(+) create mode 100644 apollo-router/src/plugins/connectors/testdata/quickstart.graphql create mode 100644 apollo-router/src/plugins/connectors/testdata/quickstart.yaml diff --git a/apollo-router/src/plugins/connectors/testdata/quickstart.graphql b/apollo-router/src/plugins/connectors/testdata/quickstart.graphql new file mode 100644 index 0000000000..ad05b4e736 --- /dev/null +++ b/apollo-router/src/plugins/connectors/testdata/quickstart.graphql @@ -0,0 +1,81 @@ +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION) + @join__directive(graphs: [CONNECTORS], name: "link", args: {url: "https://specs.apollo.dev/connect/v0.1", import: ["@connect", "@source"]}) + @join__directive(graphs: [CONNECTORS], name: "source", args: {name: "jsonPlaceholder", http: {baseURL: "https://jsonplaceholder.typicode.com/"}}) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +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, overrideLabel: String, contextArguments: [join__ContextArgument!]) 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 + +input join__ContextArgument { + name: String! + type: String! + context: String! + selection: join__FieldValue! +} + +scalar join__DirectiveArguments + +scalar join__FieldSet + +scalar join__FieldValue + +enum join__Graph { + CONNECTORS @join__graph(name: "connectors", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Post + @join__type(graph: CONNECTORS) +{ + id: ID! + body: String + title: String + author: User +} + +type Query + @join__type(graph: CONNECTORS) +{ + posts: [Post] @join__directive(graphs: [CONNECTORS], name: "connect", args: {source: "jsonPlaceholder", http: {GET: "/posts"}, selection: "id\ntitle\nbody\nauthor: { id: userId }"}) + post(id: ID!): Post @join__directive(graphs: [CONNECTORS], name: "connect", args: {source: "jsonPlaceholder", http: {GET: "/posts/{$args.id}"}, selection: "id\ntitle\nbody\nauthor: { id: userId }", entity: true}) + user(id: ID!): User @join__directive(graphs: [CONNECTORS], name: "connect", args: {source: "jsonPlaceholder", http: {GET: "/users/{$args.id}"}, selection: "id\nname\nusername", entity: true}) +} + +type User + @join__type(graph: CONNECTORS) +{ + id: ID! + name: String + username: String + posts: [Post] @join__directive(graphs: [CONNECTORS], name: "connect", args: {source: "jsonPlaceholder", http: {GET: "/users/{$this.id}/posts"}, selection: "id\ntitle\nbody"}) +} diff --git a/apollo-router/src/plugins/connectors/testdata/quickstart.yaml b/apollo-router/src/plugins/connectors/testdata/quickstart.yaml new file mode 100644 index 0000000000..b12af2b3fd --- /dev/null +++ b/apollo-router/src/plugins/connectors/testdata/quickstart.yaml @@ -0,0 +1,77 @@ +# rover supergraph compose --config apollo-router/src/plugins/connectors/testdata/quickstart.yaml > apollo-router/src/plugins/connectors/testdata/quickstart.graphql +federation_version: =2.9.0-connectors.12 +subgraphs: + connectors: + routing_url: none + schema: + sdl: | + extend schema + @link(url: "https://specs.apollo.dev/federation/v2.8") + @link( + url: "https://specs.apollo.dev/connect/v0.1" + import: ["@connect", "@source"] + ) + @source( + name: "jsonPlaceholder" + http: { baseURL: "https://jsonplaceholder.typicode.com/" } + ) + + type Post { + id: ID! + body: String + title: String + author: User + } + + type User { + id: ID! + name: String + username: String + posts: [Post] + @connect( + source: "jsonPlaceholder" + http: { GET: "/users/{$$this.id}/posts" } + selection: """ + id + title + body + """ + ) + } + + type Query { + posts: [Post] + @connect( + source: "jsonPlaceholder" + http: { GET: "/posts" } + selection: """ + id + title + body + author: { id: userId } + """ + ) + post(id: ID!): Post + @connect( + source: "jsonPlaceholder" + http: { GET: "/posts/{$$args.id}" } + selection: """ + id + title + body + author: { id: userId } + """ + entity: true + ) + user(id: ID!): User + @connect( + source: "jsonPlaceholder" + http: { GET: "/users/{$$args.id}" } + selection: """ + id + name + username + """ + entity: true + ) + } \ No newline at end of file diff --git a/apollo-router/src/plugins/connectors/tests.rs b/apollo-router/src/plugins/connectors/tests.rs index 23127c04d1..5163664e4e 100644 --- a/apollo-router/src/plugins/connectors/tests.rs +++ b/apollo-router/src/plugins/connectors/tests.rs @@ -705,11 +705,292 @@ async fn test_no_source() { ); } +mod quickstart_tests { + use super::*; + + macro_rules! map { + ($($tt:tt)*) => { + serde_json_bytes::json!($($tt)*).as_object().unwrap().clone() + }; + } + + async fn execute(query: &str, variables: JsonMap) -> (serde_json::Value, MockServer) { + let mock_server = MockServer::start().await; + Mock::given(method("GET")).and(path("/posts")).respond_with( + ResponseTemplate::new(200).set_body_json(serde_json::json!([ + { + "userId": 1, + "id": 1, + "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit", + "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto" + }, + { + "userId": 1, + "id": 2, + "title": "qui est esse", + "body": "est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla" + }] + )), + ).mount(&mock_server).await; + Mock::given(method("GET")).and(path("/posts/1")).respond_with( + ResponseTemplate::new(200).set_body_json(serde_json::json!( + { + "userId": 1, + "id": 1, + "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit", + "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto" + } + )), + ).mount(&mock_server).await; + Mock::given(method("GET")).and(path("/posts/2")).respond_with( + ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "userId": 1, + "id": 2, + "title": "qui est esse", + "body": "est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla" + } + )), + ).mount(&mock_server).await; + Mock::given(method("GET")) + .and(path("/users/1")) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "id": 1, + "name": "Leanne Graham", + "username": "Bret", + "email": "Sincere@april.biz", + "address": { + "street": "Kulas Light", + "suite": "Apt. 556", + "city": "Gwenborough", + "zipcode": "92998-3874", + "geo": { + "lat": "-37.3159", + "lng": "81.1496" + } + }, + "phone": "1-770-736-8031 x56442", + "website": "hildegard.org", + "company": { + "name": "Romaguera-Crona", + "catchPhrase": "Multi-layered client-server neural-net", + "bs": "harness real-time e-markets" + } + }))) + .mount(&mock_server) + .await; + Mock::given(method("GET")).and(path("/users/1/posts")).respond_with( + ResponseTemplate::new(200).set_body_json(serde_json::json!([ + { + "userId": 1, + "id": 1, + "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit", + "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto" + }, + { + "userId": 1, + "id": 2, + "title": "qui est esse", + "body": "est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla" + }] + )), + ).mount(&mock_server).await; + + let res = super::execute( + &QUICKSTART_SCHEMA.replace("https://jsonplaceholder.typicode.com", &mock_server.uri()), + &mock_server.uri(), + query, + variables, + None, + |_| {}, + ) + .await; + + (res, mock_server) + } + + #[tokio::test] + async fn query_1() { + let query = r#" + query Posts { + posts { + id + body + title + } + } + "#; + + let (response, server) = execute(query, Default::default()).await; + + insta::assert_json_snapshot!(response, @r###" + { + "data": { + "posts": [ + { + "id": 1, + "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto", + "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit" + }, + { + "id": 2, + "body": "est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla", + "title": "qui est esse" + } + ] + } + } + "###); + + req_asserts::matches( + &server.received_requests().await.unwrap(), + vec![Matcher::new().method("GET").path("/posts").build()], + ); + } + + #[tokio::test] + async fn query_2() { + let query = r#" + query Post($postId: ID!) { + post(id: $postId) { + id + title + body + } + } + "#; + + let (response, server) = execute(query, map!({ "postId": "1" })).await; + + insta::assert_json_snapshot!(response, @r###" + { + "data": { + "post": { + "id": 1, + "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit", + "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto" + } + } + } + "###); + + req_asserts::matches( + &server.received_requests().await.unwrap(), + vec![Matcher::new().method("GET").path("/posts/1").build()], + ); + } + + #[tokio::test] + async fn query_3() { + let query = r#" + query PostWithAuthor($postId: ID!) { + post(id: $postId) { + id + title + body + author { + id + name + } + } + } + "#; + + let (response, server) = execute(query, map!({ "postId": "1" })).await; + + insta::assert_json_snapshot!(response, @r###" + { + "data": { + "post": { + "id": 1, + "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit", + "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto", + "author": { + "id": 1, + "name": "Leanne Graham" + } + } + } + } + "###); + + req_asserts::matches( + &server.received_requests().await.unwrap(), + vec![ + Matcher::new().method("GET").path("/posts/1").build(), + Matcher::new().method("GET").path("/users/1").build(), + ], + ); + } + + #[tokio::test] + async fn query_4() { + let query = r#" + query PostsForUser($userId: ID!) { + user(id: $userId) { + id + name + posts { + id + title + author { + id + name + } + } + } + } + "#; + + let (response, server) = execute(query, map!({ "userId": "1" })).await; + + insta::assert_json_snapshot!(response, @r###" + { + "data": { + "user": { + "id": 1, + "name": "Leanne Graham", + "posts": [ + { + "id": 1, + "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit", + "author": { + "id": 1, + "name": "Leanne Graham" + } + }, + { + "id": 2, + "title": "qui est esse", + "author": { + "id": 1, + "name": "Leanne Graham" + } + } + ] + } + } + } + "###); + + req_asserts::matches( + &server.received_requests().await.unwrap(), + vec![ + Matcher::new().method("GET").path("/users/1").build(), + Matcher::new().method("GET").path("/users/1/posts").build(), + Matcher::new().method("GET").path("/posts/1").build(), + Matcher::new().method("GET").path("/posts/2").build(), + Matcher::new().method("GET").path("/users/1").build(), + ], + ); + } +} + const STEEL_THREAD_SCHEMA: &str = include_str!("./testdata/steelthread.graphql"); const MUTATION_SCHEMA: &str = include_str!("./testdata/mutation.graphql"); const NULLABILITY_SCHEMA: &str = include_str!("./testdata/nullability.graphql"); const SELECTION_SCHEMA: &str = include_str!("./testdata/selection.graphql"); const NO_SOURCES_SCHEMA: &str = include_str!("./testdata/connector-without-source.graphql"); +const QUICKSTART_SCHEMA: &str = include_str!("./testdata/quickstart.graphql"); async fn execute( schema: &str,