async-graphql/tests/federation.rs
Dominik Spicher b020ce5aee tests/federation: compare export_sdl against expected schema
This commit adds logic to two unit tests where the schema
export is compared against expected output cached in two
schema files.

This is intended to help prevent bugs like the ones fixed in
faf407b or the immediately succeeding commit, as unexpected
changes to schema export will become apparent in the commit
diff, easing reviews.

When legitimately changing the export output behaviour,
the test suite just needs to be run twice, as the unit-tests
automatically overwrite the files with the new version.

This unit-test approach is inspired by

https://matklad.github.io/2022/03/26/self-modifying-code.html
2022-08-29 22:23:28 +02:00

759 lines
20 KiB
Rust

#![allow(unreachable_code)]
use std::{collections::HashMap, convert::Infallible};
use async_graphql::{
dataloader::{DataLoader, Loader},
*,
};
#[tokio::test]
pub async fn test_nested_key() {
#[derive(InputObject)]
struct MyInputA {
a: i32,
b: i32,
c: MyInputB,
}
#[derive(InputObject)]
struct MyInputB {
v: i32,
}
assert_eq!(MyInputB::federation_fields().as_deref(), Some("{ v }"));
assert_eq!(
MyInputA::federation_fields().as_deref(),
Some("{ a b c { v } }")
);
struct Query;
#[derive(SimpleObject)]
struct MyObj {
a: i32,
b: i32,
c: i32,
}
#[Object]
impl Query {
#[graphql(entity)]
async fn find_obj(&self, input: MyInputA) -> MyObj {
MyObj {
a: input.a,
b: input.b,
c: input.c.v,
}
}
}
let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
let query = r#"{
_entities(representations: [{__typename: "MyObj", input: {a: 1, b: 2, c: { v: 3 }}}]) {
__typename
... on MyObj {
a b c
}
}
}"#;
assert_eq!(
schema.execute(query).await.into_result().unwrap().data,
value!({
"_entities": [
{"__typename": "MyObj", "a": 1, "b": 2, "c": 3},
]
})
);
}
#[tokio::test]
pub async fn test_federation() {
struct User {
id: ID,
}
#[Object(extends)]
impl User {
#[graphql(external)]
async fn id(&self) -> &ID {
&self.id
}
async fn reviews(&self) -> Vec<Review> {
todo!()
}
}
struct Review;
#[Object]
impl Review {
async fn body(&self) -> String {
todo!()
}
async fn author(&self) -> User {
todo!()
}
async fn product(&self) -> Product {
todo!()
}
}
struct Product {
upc: String,
}
#[Object(extends)]
impl Product {
#[graphql(external)]
async fn upc(&self) -> &str {
&self.upc
}
async fn reviews(&self) -> Vec<Review> {
todo!()
}
}
struct Query;
#[Object]
impl Query {
#[graphql(entity)]
async fn find_user_by_id(&self, id: ID) -> User {
User { id }
}
#[graphql(entity)]
async fn find_product_by_upc(&self, upc: String) -> Product {
Product { upc }
}
}
let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
let query = r#"{
_entities(representations: [{__typename: "Product", upc: "B00005N5PF"}]) {
__typename
... on Product {
upc
}
}
}"#;
assert_eq!(
schema.execute(query).await.into_result().unwrap().data,
value!({
"_entities": [
{"__typename": "Product", "upc": "B00005N5PF"},
]
})
);
}
#[tokio::test]
pub async fn test_find_entity_with_context() {
struct MyLoader;
#[async_trait::async_trait]
impl Loader<ID> for MyLoader {
type Value = MyObj;
type Error = Infallible;
async fn load(&self, keys: &[ID]) -> Result<HashMap<ID, Self::Value>, Self::Error> {
Ok(keys
.iter()
.filter(|id| id.as_str() != "999")
.map(|id| {
(
id.clone(),
MyObj {
id: id.clone(),
value: 999,
},
)
})
.collect())
}
}
#[derive(Clone, SimpleObject)]
struct MyObj {
id: ID,
value: i32,
}
struct Query;
#[Object]
impl Query {
#[graphql(entity)]
async fn find_user_by_id(&self, ctx: &Context<'_>, id: ID) -> FieldResult<MyObj> {
let loader = ctx.data_unchecked::<DataLoader<MyLoader>>();
loader
.load_one(id)
.await
.unwrap()
.ok_or_else(|| "Not found".into())
}
}
let schema = Schema::build(Query, EmptyMutation, EmptySubscription)
.data(DataLoader::new(MyLoader, tokio::spawn))
.finish();
let query = r#"{
_entities(representations: [
{__typename: "MyObj", id: "1"},
{__typename: "MyObj", id: "2"},
{__typename: "MyObj", id: "3"},
{__typename: "MyObj", id: "4"}
]) {
__typename
... on MyObj {
id
value
}
}
}"#;
assert_eq!(
schema.execute(query).await.into_result().unwrap().data,
value!({
"_entities": [
{"__typename": "MyObj", "id": "1", "value": 999 },
{"__typename": "MyObj", "id": "2", "value": 999 },
{"__typename": "MyObj", "id": "3", "value": 999 },
{"__typename": "MyObj", "id": "4", "value": 999 },
]
})
);
let query = r#"{
_entities(representations: [
{__typename: "MyObj", id: "999"}
]) {
__typename
... on MyObj {
id
value
}
}
}"#;
assert_eq!(
schema.execute(query).await.into_result().unwrap_err(),
vec![ServerError {
message: "Not found".to_string(),
source: None,
locations: vec![Pos {
line: 2,
column: 13
}],
path: vec![PathSegment::Field("_entities".to_owned())],
extensions: None,
}]
);
}
#[tokio::test]
pub async fn test_entity_union() {
#[derive(SimpleObject)]
struct MyObj {
a: i32,
}
struct Query;
#[Object]
impl Query {
#[graphql(entity)]
async fn find_obj(&self, _id: i32) -> MyObj {
todo!()
}
}
let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
let query = r#"{
__type(name: "_Entity") { possibleTypes { name } }
}"#;
assert_eq!(
schema.execute(query).await.into_result().unwrap().data,
value!({
"__type": {
"possibleTypes": [
{"name": "MyObj"},
]
}
})
);
}
#[tokio::test]
pub async fn test_entity_shareable() {
#[derive(SimpleObject)]
struct MyObjFieldShareable {
#[graphql(shareable)]
field_shareable_a: i32,
}
#[derive(SimpleObject)]
#[graphql(shareable)]
struct MyObjShareable {
a: i32,
}
struct Query;
#[Object(extends)]
impl Query {
#[graphql(entity)]
async fn find_obj_field_shareable(&self, _id: i32) -> MyObjFieldShareable {
todo!()
}
#[graphql(entity)]
async fn find_obj_shareable(&self, _id: i32) -> MyObjShareable {
todo!()
}
}
let schema_sdl = Schema::new(Query, EmptyMutation, EmptySubscription)
.sdl_with_options(SDLExportOptions::new().federation());
assert_eq!(
schema_sdl.contains("fieldShareableA: Int! @shareable"),
true
);
assert_eq!(
schema_sdl.contains(r#"MyObjShareable @key(fields: "id") @shareable"#),
true
);
}
#[tokio::test]
pub async fn test_field_override_directive() {
#[derive(SimpleObject)]
struct MyObjFieldOverride {
#[graphql(override_from = "AnotherSubgraph")]
field_override_a: i32,
}
struct Query;
#[Object(extends)]
impl Query {
#[graphql(entity)]
async fn find_obj_field_override(&self, _id: i32) -> MyObjFieldOverride {
todo!()
}
}
let schema_sdl = Schema::new(Query, EmptyMutation, EmptySubscription)
.sdl_with_options(SDLExportOptions::new().federation());
assert_eq!(
schema_sdl.contains("fieldOverrideA: Int! @override(from: \"AnotherSubgraph\")"),
true
);
}
#[tokio::test]
pub async fn test_entity_inaccessible() {
struct MyCustomObjInaccessible;
#[Object(inaccessible)]
impl MyCustomObjInaccessible {
async fn a(&self) -> i32 {
todo!()
}
#[graphql(inaccessible)]
async fn custom_object_inaccessible(&self) -> i32 {
todo!()
}
}
#[derive(SimpleObject)]
struct MyObjFieldInaccessible {
#[graphql(inaccessible)]
obj_field_inaccessible_a: i32,
}
#[derive(SimpleObject)]
#[graphql(inaccessible)]
struct MyObjInaccessible {
a: i32,
}
#[derive(InputObject)]
struct MyInputObjFieldInaccessible {
#[graphql(inaccessible)]
input_field_inaccessible_a: i32,
}
#[derive(InputObject)]
#[graphql(inaccessible)]
struct MyInputObjInaccessible {
a: i32,
}
#[derive(Enum, PartialEq, Eq, Copy, Clone)]
enum MyEnumVariantInaccessible {
#[graphql(inaccessible)]
OptionAInaccessible,
OptionB,
OptionC,
}
#[derive(Enum, PartialEq, Eq, Copy, Clone)]
#[graphql(inaccessible)]
enum MyEnumInaccessible {
OptionA,
OptionB,
OptionC,
}
#[derive(SimpleObject)]
struct MyInterfaceObjA {
inaccessible_interface_value: String,
}
#[derive(SimpleObject)]
#[graphql(inaccessible)]
struct MyInterfaceObjB {
inaccessible_interface_value: String,
}
#[derive(Interface)]
#[graphql(field(name = "inaccessible_interface_value", type = "String", inaccessible))]
#[graphql(inaccessible)]
enum MyInterfaceInaccessible {
MyInterfaceObjA(MyInterfaceObjA),
MyInterfaceObjB(MyInterfaceObjB),
}
#[derive(Union)]
#[graphql(inaccessible)]
enum MyUnionInaccessible {
MyInterfaceObjA(MyInterfaceObjA),
MyInterfaceObjB(MyInterfaceObjB),
}
struct MyNumberInaccessible(i32);
#[Scalar(inaccessible)]
impl ScalarType for MyNumberInaccessible {
fn parse(_value: Value) -> InputValueResult<Self> {
todo!()
}
fn to_value(&self) -> Value {
todo!()
}
}
struct Query;
#[Object(extends)]
impl Query {
#[graphql(entity)]
async fn find_obj_field_inaccessible(&self, _id: i32) -> MyObjFieldInaccessible {
todo!()
}
#[graphql(entity)]
async fn find_obj_inaccessible(&self, _id: i32) -> MyObjInaccessible {
todo!()
}
async fn enum_variant_inaccessible(&self, _id: i32) -> MyEnumVariantInaccessible {
todo!()
}
async fn enum_inaccessible(&self, _id: i32) -> MyEnumInaccessible {
todo!()
}
#[graphql(inaccessible)]
async fn inaccessible_field(&self, _id: i32) -> i32 {
todo!()
}
async fn inaccessible_argument(&self, #[graphql(inaccessible)] _id: i32) -> i32 {
todo!()
}
async fn inaccessible_interface(&self) -> MyInterfaceInaccessible {
todo!()
}
async fn inaccessible_union(&self) -> MyUnionInaccessible {
todo!()
}
async fn inaccessible_scalar(&self) -> MyNumberInaccessible {
todo!()
}
async fn inaccessible_input_field(&self, _value: MyInputObjFieldInaccessible) -> i32 {
todo!()
}
async fn inaccessible_input(&self, _value: MyInputObjInaccessible) -> i32 {
todo!()
}
async fn inaccessible_custom_object(&self) -> MyCustomObjInaccessible {
todo!()
}
}
let schema_sdl = Schema::new(Query, EmptyMutation, EmptySubscription)
.sdl_with_options(SDLExportOptions::new().federation());
// FIELD_DEFINITION
assert!(schema_sdl.contains("inaccessibleField(id: Int!): Int! @inaccessible"));
assert!(schema_sdl.contains("objFieldInaccessibleA: Int! @inaccessible"));
assert!(schema_sdl.contains("inaccessibleInterfaceValue: String! @inaccessible"));
assert!(schema_sdl.contains("customObjectInaccessible: Int! @inaccessible"));
// INTERFACE
assert!(schema_sdl.contains("interface MyInterfaceInaccessible @inaccessible"));
// OBJECT
assert!(schema_sdl.contains("type MyCustomObjInaccessible @inaccessible"));
assert!(schema_sdl.contains(r#"type MyObjInaccessible @key(fields: "id") @inaccessible"#));
assert!(schema_sdl
.contains("type MyInterfaceObjB implements MyInterfaceInaccessible @inaccessible"));
// UNION
assert!(schema_sdl.contains("union MyUnionInaccessible @inaccessible ="));
// ARGUMENT_DEFINITION
assert!(schema_sdl.contains("inaccessibleArgument(id: Int! @inaccessible): Int!"));
// SCALAR
assert!(schema_sdl.contains("scalar MyNumberInaccessible @inaccessible"));
// ENUM
assert!(schema_sdl.contains("enum MyEnumInaccessible @inaccessible"));
// ENUM_VALUE
assert!(schema_sdl.contains("OPTION_A_INACCESSIBLE @inaccessible"));
// INPUT_OBJECT
assert!(schema_sdl.contains("input MyInputObjInaccessible @inaccessible"));
// INPUT_FIELD_DEFINITION
assert!(schema_sdl.contains("inputFieldInaccessibleA: Int! @inaccessible"));
// no trailing spaces
assert!(!schema_sdl.contains(" \n"));
// compare to expected schema
let path = std::path::Path::new(&std::env::var("CARGO_MANIFEST_DIR").unwrap())
.join("tests/schemas/test_entity_inaccessible.schema.graphql");
let expected_schema = std::fs::read_to_string(&path).unwrap();
if schema_sdl != expected_schema {
std::fs::write(path, schema_sdl).unwrap();
panic!("schema was not up-to-date. rerun")
}
}
#[tokio::test]
pub async fn test_entity_tag() {
struct MyCustomObjTagged;
#[Object(
tag = "tagged",
tag = "object",
tag = "with",
tag = "multiple",
tag = "tags"
)]
impl MyCustomObjTagged {
async fn a(&self) -> i32 {
todo!()
}
#[graphql(tag = "tagged_custom_object_field")]
async fn custom_object_tagged(&self) -> i32 {
todo!()
}
}
#[derive(SimpleObject)]
struct MyObjFieldTagged {
#[graphql(tag = "tagged_field")]
obj_field_tagged_a: i32,
}
#[derive(SimpleObject)]
#[graphql(tag = "tagged_simple_object")]
struct MyObjTagged {
a: i32,
}
#[derive(InputObject)]
struct MyInputObjFieldTagged {
#[graphql(tag = "tagged_input_object_field")]
input_field_tagged_a: i32,
}
#[derive(InputObject)]
#[graphql(tag = "input_object_tag")]
struct MyInputObjTagged {
a: i32,
}
#[derive(Enum, PartialEq, Eq, Copy, Clone)]
enum MyEnumVariantTagged {
#[graphql(tag = "tagged_enum_option")]
OptionATagged,
OptionB,
OptionC,
}
#[derive(Enum, PartialEq, Eq, Copy, Clone)]
#[graphql(tag = "tagged_num")]
enum MyEnumTagged {
OptionA,
OptionB,
OptionC,
}
#[derive(SimpleObject)]
struct MyInterfaceObjA {
tagged_interface_value: String,
}
#[derive(SimpleObject)]
#[graphql(tag = "interface_object")]
struct MyInterfaceObjB {
tagged_interface_value: String,
}
#[derive(Interface)]
#[graphql(field(
name = "tagged_interface_value",
type = "String",
tag = "tagged_interface_field"
))]
#[graphql(tag = "tagged_interface")]
enum MyInterfaceTagged {
MyInterfaceObjA(MyInterfaceObjA),
MyInterfaceObjB(MyInterfaceObjB),
}
#[derive(Union)]
#[graphql(tag = "tagged_union")]
enum MyUnionTagged {
MyInterfaceObjA(MyInterfaceObjA),
MyInterfaceObjB(MyInterfaceObjB),
}
struct MyNumberTagged(i32);
#[Scalar(tag = "tagged_scalar")]
impl ScalarType for MyNumberTagged {
fn parse(_value: Value) -> InputValueResult<Self> {
todo!()
}
fn to_value(&self) -> Value {
todo!()
}
}
struct Query;
#[Object(extends)]
impl Query {
#[graphql(entity)]
async fn find_obj_field_tagged(&self, _id: i32) -> MyObjFieldTagged {
todo!()
}
#[graphql(entity)]
async fn find_obj_tagged(&self, _id: i32) -> MyObjTagged {
todo!()
}
async fn enum_variant_tagged(&self, _id: i32) -> MyEnumVariantTagged {
todo!()
}
async fn enum_tagged(&self, _id: i32) -> MyEnumTagged {
todo!()
}
#[graphql(tag = "tagged_\"field\"")]
async fn tagged_field(&self, _id: i32) -> i32 {
todo!()
}
async fn tagged_argument(&self, #[graphql(tag = "tagged_argument")] _id: i32) -> i32 {
todo!()
}
async fn tagged_interface(&self) -> MyInterfaceTagged {
todo!()
}
async fn tagged_union(&self) -> MyUnionTagged {
todo!()
}
async fn tagged_scalar(&self) -> MyNumberTagged {
todo!()
}
async fn tagged_input_field(&self, _value: MyInputObjFieldTagged) -> i32 {
todo!()
}
async fn tagged_input(&self, _value: MyInputObjTagged) -> i32 {
todo!()
}
async fn tagged_custom_object(&self) -> MyCustomObjTagged {
todo!()
}
}
let schema_sdl = Schema::new(Query, EmptyMutation, EmptySubscription)
.sdl_with_options(SDLExportOptions::new().federation());
// FIELD_DEFINITION
assert!(schema_sdl.contains(r#"taggedField(id: Int!): Int! @tag(name: "tagged_\"field\"")"#));
assert!(schema_sdl.contains(r#"objFieldTaggedA: Int! @tag(name: "tagged_field")"#));
assert!(schema_sdl
.contains(r#"taggedInterfaceValue: String! @tag(name: "tagged_interface_field")"#));
assert!(
schema_sdl.contains(r#"customObjectTagged: Int! @tag(name: "tagged_custom_object_field")"#)
);
// INTERFACE
assert!(schema_sdl.contains(r#"interface MyInterfaceTagged @tag(name: "tagged_interface")"#));
// OBJECT
assert!(schema_sdl.contains(r#"type MyCustomObjTagged @tag(name: "tagged") @tag(name: "object") @tag(name: "with") @tag(name: "multiple") @tag(name: "tags") {"#));
assert!(schema_sdl
.contains(r#"type MyObjTagged @key(fields: "id") @tag(name: "tagged_simple_object") {"#));
assert!(schema_sdl.contains(
r#"type MyInterfaceObjB implements MyInterfaceTagged @tag(name: "interface_object")"#
));
// UNION
assert!(schema_sdl.contains(r#"union MyUnionTagged @tag(name: "tagged_union") ="#));
// ARGUMENT_DEFINITION
assert!(schema_sdl.contains(r#"taggedArgument(id: Int! @tag(name: "tagged_argument")): Int!"#));
// SCALAR
assert!(schema_sdl.contains(r#"scalar MyNumberTagged @tag(name: "tagged_scalar")"#));
// ENUM
assert!(schema_sdl.contains(r#"enum MyEnumTagged @tag(name: "tagged_num")"#));
// ENUM_VALUE
assert!(schema_sdl.contains(r#"OPTION_A_TAGGED @tag(name: "tagged_enum_option")"#));
// INPUT_OBJECT
assert!(schema_sdl.contains(r#"input MyInputObjTagged @tag(name: "input_object_tag")"#));
// INPUT_FIELD_DEFINITION
assert!(
schema_sdl.contains(r#"inputFieldTaggedA: Int! @tag(name: "tagged_input_object_field")"#)
);
// no trailing spaces
assert!(!schema_sdl.contains(" \n"));
// compare to expected schema
let path = std::path::Path::new(&std::env::var("CARGO_MANIFEST_DIR").unwrap())
.join("tests/schemas/test_entity_tag.schema.graphql");
let expected_schema = std::fs::read_to_string(&path).unwrap();
if schema_sdl != expected_schema {
std::fs::write(path, schema_sdl).unwrap();
panic!("schema was not up-to-date. rerun")
}
}