use apollo_compiler::coord;
use apollo_federation::composition::Satisfiable;
use apollo_federation::subgraph::typestate::Subgraph;
use apollo_federation::supergraph::Supergraph;
use itertools::Itertools;
use test_log::test;
use super::ServiceDefinition;
use super::assert_composition_errors;
use super::compose;
use super::compose_as_fed2_subgraphs;
fn validate_tag_propagation(supergraph: &Supergraph<Satisfiable>) {
let schema = supergraph.schema().schema();
let users_field = coord!(Query.users)
.lookup_field(schema)
.expect("Query.users should exist");
let users_field_tags = users_field
.directives
.iter()
.filter(|d| d.name == "tag")
.join(" ");
assert_eq!(
users_field_tags, r#"@tag(name: "aTaggedOperation")"#,
"Query.users should have correct @tag directive"
);
let user_type = coord!(User)
.lookup(schema)
.expect("User type should exist")
.as_object()
.expect("User should be an object type");
let user_type_tags = user_type
.directives
.iter()
.filter(|d| d.name == "tag")
.map(|d| d.to_string())
.join(" ");
assert_eq!(
user_type_tags, r#"@tag(name: "aTaggedType")"#,
"User type should have correct @tag directive"
);
let user_name_field = coord!(User.name)
.lookup_field(schema)
.expect("User.name field should exist");
let user_name_field_tags = user_name_field
.directives
.iter()
.filter(|d| d.name == "tag")
.join(" ");
assert_eq!(
user_name_field_tags, r#"@tag(name: "aTaggedField")"#,
"User.name should have correct @tag directive"
);
}
fn validate_tag_merging(supergraph: &Supergraph<Satisfiable>) {
let schema = supergraph.schema().schema();
let user_type = coord!(User)
.lookup(schema)
.expect("User type should exist")
.as_object()
.expect("User should be an object type");
let user_type_tags = user_type
.directives
.iter()
.filter(|d| d.name == "tag")
.map(|d| d.to_string())
.join(" ");
assert_eq!(
user_type_tags,
r#"@tag(name: "aTagOnTypeFromSubgraphA") @tag(name: "aMergedTagOnType") @tag(name: "aTagOnTypeFromSubgraphB")"#,
"User type should have merged @tag directives"
);
let first_name_field = coord!(Name.firstName)
.lookup_field(schema)
.expect("Name.firstName field should exist");
let field_tag_directives = first_name_field
.directives
.iter()
.filter(|d| d.name == "tag")
.join(" ");
assert_eq!(
field_tag_directives,
r#"@tag(name: "aTagOnFieldFromSubgraphA") @tag(name: "aTagOnFieldFromSubgraphB")"#,
"Name.firstName should have merged @tag directives"
);
}
#[test]
fn tag_propagates_to_supergraph_fed2_subgraphs() {
let subgraph_a = ServiceDefinition {
name: "subgraphA",
type_defs: r#"
type Query {
users: [User] @tag(name: "aTaggedOperation")
}
type User @key(fields: "id") {
id: ID!
name: String! @tag(name: "aTaggedField")
}
"#,
};
let subgraph_b = ServiceDefinition {
name: "subgraphB",
type_defs: r#"
type User @key(fields: "id") @tag(name: "aTaggedType") {
id: ID!
birthdate: String!
age: Int!
}
"#,
};
let result = compose_as_fed2_subgraphs(&[subgraph_a, subgraph_b]);
let supergraph = result.expect("Expected composition to succeed");
validate_tag_propagation(&supergraph);
}
#[test]
fn tag_propagates_to_supergraph_fed1_subgraphs() {
let subgraph_a = Subgraph::parse(
"subgraphA",
"",
r#"
type Query {
users: [User] @tag(name: "aTaggedOperation")
}
type User @key(fields: "id") {
id: ID!
name: String! @tag(name: "aTaggedField")
}
"#,
)
.unwrap();
let subgraph_b = Subgraph::parse(
"subgraphB",
"",
r#"
type User @key(fields: "id") @tag(name: "aTaggedType") {
id: ID!
birthdate: String!
age: Int!
}
"#,
)
.unwrap();
let supergraph =
compose(vec![subgraph_a, subgraph_b]).expect("Expected composition to succeed");
validate_tag_propagation(&supergraph);
}
#[test]
fn tag_propagates_to_supergraph_mixed_fed1_fed2_subgraphs() {
let subgraph_a = Subgraph::parse(
"subgraphA",
"",
r#"
type Query {
users: [User] @tag(name: "aTaggedOperation")
}
type User @key(fields: "id") {
id: ID!
name: String! @tag(name: "aTaggedField")
}
"#,
)
.unwrap();
let subgraph_b = Subgraph::parse(
"subgraphB",
"",
r#"
type User @key(fields: "id") @tag(name: "aTaggedType") {
id: ID!
birthdate: String!
age: Int!
}
"#,
)
.unwrap()
.into_fed2_test_subgraph(true)
.unwrap();
let supergraph =
compose(vec![subgraph_a, subgraph_b]).expect("Expected composition to succeed");
validate_tag_propagation(&supergraph);
}
#[test]
fn tag_merges_multiple_tags_fed2_subgraphs() {
let subgraph_a = ServiceDefinition {
name: "subgraphA",
type_defs: r#"
type Query {
user: [User]
}
type User @key(fields: "id") @tag(name: "aTagOnTypeFromSubgraphA") @tag(name: "aMergedTagOnType") {
id: ID!
name1: Name!
}
type Name @shareable {
firstName: String @tag(name: "aTagOnFieldFromSubgraphA")
lastName: String @tag(name: "aMergedTagOnField")
}
"#,
};
let subgraph_b = ServiceDefinition {
name: "subgraphB",
type_defs: r#"
type User @key(fields: "id") @tag(name: "aTagOnTypeFromSubgraphB") @tag(name: "aMergedTagOnType") {
id: ID!
name2: String!
}
type Name @shareable {
firstName: String @tag(name: "aTagOnFieldFromSubgraphB")
lastName: String @tag(name: "aMergedTagOnField")
}
"#,
};
let result = compose_as_fed2_subgraphs(&[subgraph_a, subgraph_b]);
let supergraph = result.expect("Expected composition to succeed");
validate_tag_merging(&supergraph);
}
#[test]
fn tag_merges_multiple_tags_fed1_subgraphs() {
let subgraph_a = Subgraph::parse("subgraphA", "",
r#"
type Query {
user: [User]
}
type User @key(fields: "id") @tag(name: "aTagOnTypeFromSubgraphA") @tag(name: "aMergedTagOnType") {
id: ID!
name1: Name!
}
type Name {
firstName: String @tag(name: "aTagOnFieldFromSubgraphA")
lastName: String @tag(name: "aMergedTagOnField")
}
"#,
).unwrap();
let subgraph_b = Subgraph::parse( "subgraphB", "",
r#"
type User @key(fields: "id") @tag(name: "aTagOnTypeFromSubgraphB") @tag(name: "aMergedTagOnType") {
id: ID!
name2: String!
}
type Name {
firstName: String @tag(name: "aTagOnFieldFromSubgraphB")
lastName: String @tag(name: "aMergedTagOnField")
}
"#,
).unwrap();
let supergraph =
compose(vec![subgraph_a, subgraph_b]).expect("Expected composition to succeed");
validate_tag_merging(&supergraph);
}
#[test]
fn tag_merges_multiple_tags_mixed_fed1_fed2_subgraphs() {
let subgraph_a = Subgraph::parse("subgraphA", "",
r#"
type Query {
user: [User]
}
type User @key(fields: "id") @tag(name: "aTagOnTypeFromSubgraphA") @tag(name: "aMergedTagOnType") {
id: ID!
name1: Name!
}
type Name {
firstName: String @tag(name: "aTagOnFieldFromSubgraphA")
lastName: String @tag(name: "aMergedTagOnField")
}
"#,
).unwrap();
let subgraph_b = Subgraph::parse( "subgraphB", "",
r#"
type User @key(fields: "id") @tag(name: "aTagOnTypeFromSubgraphB") @tag(name: "aMergedTagOnType") {
id: ID!
name2: String!
}
type Name @shareable {
firstName: String @tag(name: "aTagOnFieldFromSubgraphB")
lastName: String @tag(name: "aMergedTagOnField")
}
"#,
).unwrap().into_fed2_test_subgraph(true).unwrap();
let supergraph =
compose(vec![subgraph_a, subgraph_b]).expect("Expected composition to succeed");
validate_tag_merging(&supergraph);
}
#[test]
fn tag_rejects_tag_and_external_together_fed2_subgraphs() {
let subgraph_a = ServiceDefinition {
name: "subgraphA",
type_defs: r#"
type Query {
user: [User]
}
type User @key(fields: "id") {
id: ID!
name: String!
birthdate: Int! @external @tag(name: "myTag")
age: Int! @requires(fields: "birthdate")
}
"#,
};
let subgraph_b = ServiceDefinition {
name: "subgraphB",
type_defs: r#"
type User @key(fields: "id") {
id: ID!
birthdate: Int!
}
"#,
};
let result = compose_as_fed2_subgraphs(&[subgraph_a, subgraph_b]);
assert_composition_errors(
&result,
&[(
"MERGED_DIRECTIVE_APPLICATION_ON_EXTERNAL",
r#"[subgraphA] Cannot apply merged directive @tag(name: "myTag") to external field "User.birthdate""#,
)],
);
}
#[test]
fn tag_rejects_tag_and_external_together_fed1_subgraphs() {
let subgraph_a = Subgraph::parse(
"subgraphA",
"",
r#"
type Query {
user: [User]
}
type User @key(fields: "id") {
id: ID!
name: String!
birthdate: Int! @external @tag(name: "myTag")
age: Int! @requires(fields: "birthdate")
}
"#,
)
.unwrap();
let subgraph_b = Subgraph::parse(
"subgraphB",
"",
r#"
type User @key(fields: "id") {
id: ID!
birthdate: Int!
}
"#,
)
.unwrap();
let result = compose(vec![subgraph_a, subgraph_b]);
assert_composition_errors(
&result,
&[(
"MERGED_DIRECTIVE_APPLICATION_ON_EXTERNAL",
r#"[subgraphA] Cannot apply merged directive @tag(name: "myTag") to external field "User.birthdate""#,
)],
);
}
#[test]
fn tag_rejects_tag_and_external_together_mixed_fed1_fed2_subgraphs() {
let subgraph_a = Subgraph::parse(
"subgraphA",
"",
r#"
type Query {
user: [User]
}
type User @key(fields: "id") {
id: ID!
name: String!
birthdate: Int! @external @tag(name: "myTag")
age: Int! @requires(fields: "birthdate")
}
"#,
)
.unwrap();
let subgraph_b = Subgraph::parse(
"subgraphB",
"",
r#"
type User @key(fields: "id") {
id: ID!
birthdate: Int!
}
"#,
)
.unwrap()
.into_fed2_test_subgraph(true)
.unwrap();
let result = compose(vec![subgraph_a, subgraph_b]);
assert_composition_errors(
&result,
&[(
"MERGED_DIRECTIVE_APPLICATION_ON_EXTERNAL",
r#"[subgraphA] Cannot apply merged directive @tag(name: "myTag") to external field "User.birthdate""#,
)],
);
}
#[test]
fn tag_errors_if_imported_under_mismatched_names() {
let subgraph_a = Subgraph::parse(
"subgraphA",
"",
r#"
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.0", import: [{name: "@tag", as: "@apolloTag"}])
type Query {
q1: Int @apolloTag(name: "t1")
}
"#,
)
.unwrap();
let subgraph_b = Subgraph::parse(
"subgraphB",
"",
r#"
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@tag"])
type Query {
q2: Int @tag(name: "t2")
}
"#,
)
.unwrap();
let result = compose(vec![subgraph_a, subgraph_b]);
assert_composition_errors(
&result,
&[(
"LINK_IMPORT_NAME_MISMATCH",
r#"The "@tag" directive (from https://specs.apollo.dev/federation/v2.0) is imported with mismatched name between subgraphs: it is imported as "@tag" in subgraph "subgraphB" but "@apolloTag" in subgraph "subgraphA""#,
)],
);
}
#[test]
fn tag_succeeds_if_imported_under_same_non_default_name() {
let subgraph_a = Subgraph::parse(
"subgraphA",
"",
r#"
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.0", import: [{name: "@tag", as: "@apolloTag"}])
type Query {
q1: Int @apolloTag(name: "t1")
}
"#,
)
.unwrap();
let subgraph_b = Subgraph::parse(
"subgraphB",
"",
r#"
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.0", import: [{name: "@tag", as: "@apolloTag"}])
type Query {
q2: Int @apolloTag(name: "t2")
}
"#,
)
.unwrap();
let result = compose(vec![subgraph_a, subgraph_b]);
let supergraph =
result.expect("Expected composition to succeed with consistent @tag import names");
let q1 = coord!(Query.q1)
.lookup_field(supergraph.schema().schema())
.unwrap();
assert!(
q1.directives
.iter()
.any(|d| d.to_string() == r#"@apolloTag(name: "t1")"#)
);
let q2 = coord!(Query.q2)
.lookup_field(supergraph.schema().schema())
.unwrap();
assert!(
q2.directives
.iter()
.any(|d| d.to_string() == r#"@apolloTag(name: "t2")"#)
);
}