use std::collections::HashMap;
use std::sync::LazyLock;
use apollo_compiler::Name;
use apollo_compiler::ast::DirectiveList;
use apollo_compiler::ast::Type;
use apollo_compiler::collections::HashSet;
use apollo_compiler::executable::Field;
use apollo_compiler::executable::FieldSet;
use apollo_compiler::executable::Selection;
use apollo_compiler::executable::SelectionSet;
use apollo_compiler::schema::ExtendedType;
use apollo_compiler::validation::Valid;
use regex::Regex;
use crate::bail;
use crate::error::FederationError;
use crate::error::MultipleFederationErrors;
use crate::error::SingleFederationError;
use crate::schema::FederationSchema;
use crate::schema::FromContextDirective;
use crate::schema::position::CompositeTypeDefinitionPosition;
use crate::schema::position::FieldArgumentDefinitionPosition;
use crate::schema::position::HasAppliedDirectives;
use crate::schema::position::InterfaceTypeDefinitionPosition;
use crate::schema::position::ObjectTypeDefinitionPosition;
use crate::schema::position::TypeDefinitionPosition;
use crate::schema::subgraph_metadata::SubgraphMetadata;
use crate::schema::validators::DeniesAliases;
use crate::schema::validators::DeniesDirectiveApplications;
use crate::schema::validators::DenyAliases;
use crate::schema::validators::DenyFieldsWithDirectiveApplications;
use crate::schema::validators::SchemaFieldSetValidator;
use crate::utils::FallibleIterator;
use crate::utils::iter_into_single_item;
pub(crate) fn validate_from_context_directives(
schema: &FederationSchema,
meta: &SubgraphMetadata,
context_map: &HashMap<String, Vec<Name>>,
errors: &mut MultipleFederationErrors,
) -> Result<(), FederationError> {
let from_context_rules: Vec<Box<dyn FromContextValidator>> = vec![
Box::new(DenyOnAbstractType::new()),
Box::new(DenyOnInterfaceImplementation::new()),
Box::new(RequireContextExists::new(context_map)),
Box::new(RequireResolvableKey::new()),
Box::new(DenyDefaultValues::new()),
];
let Ok(from_context_directives) = schema.from_context_directive_applications() else {
return Ok(());
};
for from_context_directive in from_context_directives {
match from_context_directive {
Ok(from_context) => {
let field = from_context.arguments.field.to_string();
let (context, selection) = parse_context(&field);
for rule in from_context_rules.iter() {
rule.validate(
&from_context.target,
schema,
meta,
&context,
&selection,
errors,
)?;
}
let (Some(context), Some(selection)) = (&context, &selection) else {
continue;
};
if let Some(set_context_locations) = context_map.get(context)
&& let Err(validation_error) = validate_field_value(
context,
selection,
&from_context,
set_context_locations,
schema,
errors,
)
{
errors.push(validation_error);
}
}
Err(e) => errors.push(e),
}
}
Ok(())
}
pub(crate) fn parse_context(field: &str) -> (Option<String>, Option<String>) {
fn strip_leading_ignored_tokens(input: &str) -> Option<&str> {
iter_into_single_item(CONTEXT_PARSING_LEADING_PATTERN.captures_iter(input))
.and_then(|c| c.get(1))
.map(|m| m.as_str())
}
let Some(dollar_start) = strip_leading_ignored_tokens(field) else {
return (None, None);
};
let mut dollar_iter = dollar_start.chars();
if dollar_iter.next() != Some('$') {
return (None, Some(dollar_start.to_string()));
}
let after_dollar = dollar_iter.as_str();
let Some(context_start) = strip_leading_ignored_tokens(after_dollar) else {
return (None, None);
};
let Some(context_captures) =
iter_into_single_item(CONTEXT_PARSING_CONTEXT_PATTERN.captures_iter(context_start))
else {
return (None, None);
};
let context = match context_captures.get(1).map(|m| m.as_str()) {
Some(context) if !context.is_empty() => context,
_ => {
return (None, None);
}
};
let selection = match context_captures.get(2).map(|m| m.as_str()) {
Some(selection) => {
let Some(selection) = strip_leading_ignored_tokens(selection) else {
return (Some(context.to_owned()), None);
};
if !selection.is_empty() {
selection
} else {
return (Some(context.to_owned()), None);
}
}
_ => {
return (Some(context.to_owned()), None);
}
};
let Some(selection) = strip_leading_ignored_tokens(selection) else {
return (Some(context.to_owned()), None);
};
(Some(context.to_owned()), Some(selection.to_owned()))
}
static CONTEXT_PARSING_LEADING_PATTERN: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r#"^(?:[\n\r\t ,]|#[^\n\r]*)*((?s:.)*)$"#).unwrap());
static CONTEXT_PARSING_CONTEXT_PATTERN: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r#"^([A-Za-z_](?-u:\w)*)((?s:.)*)$"#).unwrap());
#[derive(Debug, PartialEq)]
enum SelectionType {
Error,
Field,
InlineFragment {
type_conditions: std::collections::HashSet<String>,
},
}
fn validate_selection_format(
context: &str,
selection_set: &SelectionSet,
from_context_parent: &FieldArgumentDefinitionPosition,
errors: &mut MultipleFederationErrors,
) -> SelectionType {
let mut type_conditions = std::collections::HashSet::new();
let mut has_field = false;
let mut has_inline_fragment = false;
for selection in selection_set.selections.iter() {
match selection {
Selection::Field(_) => {
has_field = true;
}
Selection::InlineFragment(fragment) => {
has_inline_fragment = true;
if let Some(type_condition) = &fragment.type_condition {
type_conditions.insert(type_condition.to_string());
} else {
errors.push(
SingleFederationError::ContextSelectionInvalid {
message: format!(
"Context \"{context}\" is used in \"{from_context_parent}\" but the selection is invalid: inline fragments must have type conditions"
),
}
.into(),
);
return SelectionType::Error;
}
}
Selection::FragmentSpread(_) => {
errors.push(
SingleFederationError::ContextSelectionInvalid {
message: format!(
"Context \"{context}\" is used in \"{from_context_parent}\" but the selection is invalid: fragment spreads are not allowed"
),
}
.into(),
);
return SelectionType::Error;
}
}
}
if has_field && has_inline_fragment {
errors.push(
SingleFederationError::ContextSelectionInvalid {
message: format!("Context \"{context}\" is used in \"{from_context_parent}\" but the selection is invalid: multiple fields could be selected"),
}
.into(),
);
return SelectionType::Error;
} else if has_field {
return SelectionType::Field;
}
if type_conditions.len() != selection_set.selections.len() {
errors.push(
SingleFederationError::ContextSelectionInvalid {
message: format!(
"Context \"{context}\" is used in \"{from_context_parent}\" but the selection is invalid: type conditions have the same name"
),
}
.into(),
);
return SelectionType::Error;
}
SelectionType::InlineFragment { type_conditions }
}
fn validate_field_value(
context: &str,
selection: &String,
applied_directive: &FromContextDirective,
set_context_locations: &[Name],
schema: &FederationSchema,
errors: &mut MultipleFederationErrors,
) -> Result<(), FederationError> {
let argument_rules: Vec<Box<dyn SchemaFieldSetValidator<FromContextDirective>>> = vec![
Box::new(DenyAliases::new()),
Box::new(DenyFieldsWithDirectiveApplications::new()),
];
let target = &applied_directive.target;
let expected_type = match target {
FieldArgumentDefinitionPosition::Object(pos) => match pos.get(schema.schema()) {
Ok(arg_def) => arg_def.ty.item_type(),
Err(_) => bail!("could not find position in schema"),
},
FieldArgumentDefinitionPosition::Interface(pos) => match pos.get(schema.schema()) {
Ok(arg_def) => arg_def.ty.item_type(),
Err(_) => bail!("could not find position in schema"),
},
};
let mut used_type_conditions: HashSet<String> = Default::default();
let mut selection_type = SelectionType::Error;
for location_name in set_context_locations {
let Some(extended_type) = schema.schema().types.get(location_name) else {
continue;
};
let Ok(location) =
CompositeTypeDefinitionPosition::try_from(TypeDefinitionPosition::from(extended_type))
else {
continue;
};
let result = FieldSet::parse(
Valid::assume_valid_ref(schema.schema()),
location.type_name().clone(),
selection,
"from_context.graphql",
);
if result.is_err() {
errors.push(
SingleFederationError::ContextSelectionInvalid {
message: format!(
"Context \"{}\" is used in \"{}\" but the selection is invalid for type \"{}\".",
context, target, location.type_name().clone(),
),
}
.into(),
);
return Ok(());
}
let fields = result.unwrap();
for rule in argument_rules.iter() {
rule.visit(location_name, &fields, applied_directive, errors);
}
if !errors.errors.is_empty() {
return Ok(());
}
selection_type = validate_selection_format(context, &fields.selection_set, target, errors);
if selection_type == SelectionType::Error {
return Ok(());
}
let selection_set = &fields.selection_set;
if selection_type == SelectionType::Field && selection_set.selections.len() > 1 {
errors.push(
SingleFederationError::ContextSelectionInvalid {
message: format!(
"Context \"{context}\" is used in \"{target}\" but the selection is invalid: multiple selections are made"
),
}
.into(),
);
return Ok(());
}
match &selection_type {
SelectionType::Field => {
let type_position = TypeDefinitionPosition::from(location.clone());
let resolved_type = validate_field_value_type(
context,
&type_position,
selection_set,
schema,
target,
errors,
)?;
let Some(resolved_type) = resolved_type else {
errors.push(
SingleFederationError::ContextSelectionInvalid {
message: format!(
"Context \"{context}\" is used in \"{target}\" but the selection is invalid: the type of the selection does not match the expected type \"{expected_type}\""
),
}
.into(),
);
return Ok(());
};
if !is_valid_implementation_field_type(&resolved_type, expected_type) {
errors.push(
SingleFederationError::ContextSelectionInvalid {
message: format!(
"Context \"{context}\" is used in \"{target}\" but the selection is invalid: the type of the selection \"{resolved_type}\" does not match the expected type \"{expected_type}\""
),
}
.into(),
);
return Ok(());
}
}
SelectionType::InlineFragment { type_conditions } => {
for selection in selection_set.selections.iter() {
if let Selection::InlineFragment(frag) = selection
&& let Some(type_condition) = &frag.type_condition
{
let Some(extended_type) =
schema.schema().types.get(type_condition.as_str())
else {
errors.push(
SingleFederationError::ContextSelectionInvalid { message: format!(
"Inline fragment type condition invalid. Type '{}' does not exist in schema.", type_condition.as_str()
) }
.into(),
);
continue;
};
let frag_type_position = TypeDefinitionPosition::from(extended_type);
if ObjectTypeDefinitionPosition::try_from(frag_type_position.clone())
.is_err()
{
errors.push(
SingleFederationError::ContextSelectionInvalid { message:
"Inline fragment type condition invalid: type conditions must be an object type".to_string()
}.into(),
);
continue;
}
if let Ok(Some(resolved_type)) = validate_field_value_type(
context,
&frag_type_position,
&frag.selection_set,
schema,
target,
errors,
) {
if !is_valid_implementation_field_type(expected_type, &resolved_type) {
errors.push(
SingleFederationError::ContextSelectionInvalid {
message: format!(
"Context \"{context}\" is used in \"{target}\" but the selection is invalid: the type of the selection \"{resolved_type}\" does not match the expected type \"{expected_type}\""
),
}
.into(),
);
return Ok(());
}
used_type_conditions.insert(type_condition.as_str().to_string());
} else {
errors.push(
SingleFederationError::ContextSelectionInvalid {
message: format!(
"Context \"{context}\" is used in \"{target}\" but the selection is invalid: the type of the selection does not match the expected type \"{expected_type}\""
),
}
.into(),
);
return Ok(());
}
}
}
let context_location_names: std::collections::HashSet<String> =
set_context_locations
.iter()
.map(|name| name.as_str().to_string())
.collect();
let mut has_matching_condition = false;
for type_condition in type_conditions {
if context_location_names.contains(type_condition) {
has_matching_condition = true;
break;
} else {
let Some(type_condition_type) =
schema.schema().types.get(type_condition.as_str())
else {
bail!("Type not found for type condition: {}", type_condition);
};
let interfaces = match type_condition_type {
ExtendedType::Interface(intf) => intf
.implements_interfaces
.iter()
.map(|i| i.name.clone())
.collect(),
ExtendedType::Object(obj) => obj
.implements_interfaces
.iter()
.map(|i| i.name.clone())
.collect(),
_ => vec![],
};
if interfaces
.iter()
.any(|itf| context_location_names.contains(itf.as_str()))
{
has_matching_condition = true;
break;
}
}
}
if !has_matching_condition && !type_conditions.is_empty() {
let context_locations_str = set_context_locations
.iter()
.map(|name| name.as_str())
.collect::<Vec<_>>()
.join(", ");
errors.push(
SingleFederationError::ContextSelectionInvalid {
message: format!(
"Context \"{context}\" is used in \"{target}\" but the selection is invalid: no type condition matches the location \"{context_locations_str}\""
),
}
.into(),
);
}
}
SelectionType::Error => return Ok(()),
}
}
if let SelectionType::InlineFragment { type_conditions } = selection_type {
for type_condition in &type_conditions {
if !used_type_conditions.contains(type_condition) {
errors.push(
SingleFederationError::ContextSelectionInvalid {
message: format!(
"Context \"{context}\" is used in \"{target}\" but the selection is invalid: type condition \"{type_condition}\" is never used"
),
}
.into(),
);
}
}
}
Ok(())
}
fn is_valid_implementation_field_type(field_type: &Type, implemented_field_type: &Type) -> bool {
match (field_type, implemented_field_type) {
(Type::NonNullNamed(field_name), Type::NonNullNamed(impl_name)) => {
let field_type_nullable = Type::Named(field_name.clone());
let implemented_field_type_nullable = Type::Named(impl_name.clone());
is_valid_implementation_field_type(
&field_type_nullable,
&implemented_field_type_nullable,
)
}
(Type::NonNullNamed(field_name), Type::Named(_)) => {
let field_type_nullable = Type::Named(field_name.clone());
is_valid_implementation_field_type(&field_type_nullable, implemented_field_type)
}
(Type::NonNullList(field_inner), Type::NonNullList(impl_inner)) => {
let field_type_nullable = (**field_inner).clone();
let implemented_field_type_nullable = (**impl_inner).clone();
is_valid_implementation_field_type(
&field_type_nullable,
&implemented_field_type_nullable,
)
}
(Type::NonNullList(field_inner), Type::List(_)) => {
let field_type_nullable = (**field_inner).clone();
is_valid_implementation_field_type(&field_type_nullable, implemented_field_type)
}
(Type::List(field_inner), Type::List(impl_inner)) => {
let field_type_inner = (**field_inner).clone();
let implemented_type_inner = (**impl_inner).clone();
is_valid_implementation_field_type(&field_type_inner, &implemented_type_inner)
}
(Type::Named(field_name), Type::Named(impl_name)) => field_name == impl_name,
_ => false,
}
}
trait FromContextValidator {
fn validate(
&self,
target: &FieldArgumentDefinitionPosition,
schema: &FederationSchema,
meta: &SubgraphMetadata,
context: &Option<String>,
selection: &Option<String>,
errors: &mut MultipleFederationErrors,
) -> Result<(), FederationError>;
}
struct DenyOnAbstractType {}
impl DenyOnAbstractType {
fn new() -> Self {
Self {}
}
}
impl FromContextValidator for DenyOnAbstractType {
fn validate(
&self,
target: &FieldArgumentDefinitionPosition,
_schema: &FederationSchema,
_meta: &SubgraphMetadata,
_context: &Option<String>,
_selection: &Option<String>,
errors: &mut MultipleFederationErrors,
) -> Result<(), FederationError> {
if let FieldArgumentDefinitionPosition::Interface(_) = target {
errors.push(
SingleFederationError::ContextNotSet {
message: format!(
"@fromContext argument cannot be used on a field that exists on an abstract type \"{target}\"."
),
}
.into(),
);
}
Ok(())
}
}
struct DenyOnInterfaceImplementation {}
impl DenyOnInterfaceImplementation {
fn new() -> Self {
Self {}
}
}
impl FromContextValidator for DenyOnInterfaceImplementation {
fn validate(
&self,
target: &FieldArgumentDefinitionPosition,
schema: &FederationSchema,
_meta: &SubgraphMetadata,
_context: &Option<String>,
_selection: &Option<String>,
errors: &mut MultipleFederationErrors,
) -> Result<(), FederationError> {
if let FieldArgumentDefinitionPosition::Object(position) = target {
let obj = position.parent().parent().get(schema.schema())?;
let field = position.parent().field_name;
for implemented in &obj.implements_interfaces {
let itf = InterfaceTypeDefinitionPosition {
type_name: implemented.name.clone(),
};
let field = itf.fields(schema.schema())?.find(|f| f.field_name == field);
if field.is_some() {
errors.push(
SingleFederationError::ContextNotSet {
message: format!(
"@fromContext argument cannot be used on a field implementing an interface field \"{}.{}\".",
itf.type_name,
field.unwrap().field_name,
),
}
.into(),
);
}
}
}
Ok(())
}
}
struct RequireContextExists<'a> {
context_map: &'a HashMap<String, Vec<Name>>,
}
impl<'a> RequireContextExists<'a> {
fn new(context_map: &'a HashMap<String, Vec<Name>>) -> Self {
Self { context_map }
}
}
impl<'a> FromContextValidator for RequireContextExists<'a> {
fn validate(
&self,
target: &FieldArgumentDefinitionPosition,
_schema: &FederationSchema,
_meta: &SubgraphMetadata,
context: &Option<String>,
selection: &Option<String>,
errors: &mut MultipleFederationErrors,
) -> Result<(), FederationError> {
let context = context.as_ref().map(|s| s.as_str()).unwrap_or("");
let selection = selection.as_ref().map(|s| s.as_str()).unwrap_or("");
if context.is_empty() {
errors.push(
SingleFederationError::NoContextReferenced {
message: format!(
"@fromContext argument does not reference a context \"{selection}\"."
),
}
.into(),
);
} else if !self.context_map.contains_key(context) {
errors.push(
SingleFederationError::ContextNotSet {
message: format!(
"Context \"{context}\" is used at location \"{target}\" but is never set."
),
}
.into(),
);
} else if selection.is_empty() {
errors.push(
SingleFederationError::NoSelectionForContext {
message: format!(
"@fromContext directive in field \"{target}\" has no selection"
),
}
.into(),
);
}
Ok(())
}
}
struct RequireResolvableKey {}
impl RequireResolvableKey {
fn new() -> Self {
Self {}
}
}
impl FromContextValidator for RequireResolvableKey {
fn validate(
&self,
target: &FieldArgumentDefinitionPosition,
schema: &FederationSchema,
meta: &SubgraphMetadata,
_context: &Option<String>,
_selection: &Option<String>,
errors: &mut MultipleFederationErrors,
) -> Result<(), FederationError> {
if let FieldArgumentDefinitionPosition::Object(position) = target {
let parent = position.parent().parent();
let key_directive = meta
.federation_spec_definition()
.key_directive_definition(schema)?;
if parent
.get_applied_directives(schema, &key_directive.name)
.iter()
.fallible_filter(|application| -> Result<bool, FederationError> {
let arguments = meta
.federation_spec_definition()
.key_directive_arguments(application)?;
Ok(arguments.resolvable)
})
.collect::<Result<Vec<_>, _>>()?
.is_empty()
{
errors.push(
SingleFederationError::ContextNoResolvableKey {
message: format!(
"Object \"{parent}\" has no resolvable key but has a field with a contextual argument."
),
}
.into(),
);
}
}
Ok(())
}
}
struct DenyDefaultValues {}
impl DenyDefaultValues {
fn new() -> Self {
Self {}
}
}
impl FromContextValidator for DenyDefaultValues {
fn validate(
&self,
target: &FieldArgumentDefinitionPosition,
schema: &FederationSchema,
_meta: &SubgraphMetadata,
_context: &Option<String>,
_selection: &Option<String>,
errors: &mut MultipleFederationErrors,
) -> Result<(), FederationError> {
let has_default = match target {
FieldArgumentDefinitionPosition::Object(position) => {
if let Ok(arg_def) = position.get(schema.schema()) {
arg_def.default_value.is_some()
} else {
false
}
}
FieldArgumentDefinitionPosition::Interface(position) => {
if let Ok(arg_def) = position.get(schema.schema()) {
arg_def.default_value.is_some()
} else {
false
}
}
};
if has_default {
errors.push(
SingleFederationError::ContextSelectionInvalid {
message: format!(
"@fromContext arguments may not have a default value: \"{target}\"."
),
}
.into(),
);
}
Ok(())
}
}
impl DeniesAliases for FromContextDirective<'_> {
fn error(&self, _alias: &Name, _field: &Field) -> SingleFederationError {
let (context, _) = parse_context(self.arguments.field);
SingleFederationError::ContextSelectionInvalid {
message: format!(
"Context \"{}\" is used in \"{}\" but the selection is invalid: aliases are not allowed in the selection",
context.unwrap_or("unknown".to_string()),
self.target
),
}
}
}
impl DeniesDirectiveApplications for FromContextDirective<'_> {
fn error(&self, _: &DirectiveList) -> SingleFederationError {
let (context, _) = parse_context(self.arguments.field);
SingleFederationError::ContextSelectionInvalid {
message: format!(
"Context \"{}\" is used in \"{}\" but the selection is invalid: directives are not allowed in the selection",
context.unwrap_or("unknown".to_string()),
self.target
),
}
}
}
#[allow(dead_code, clippy::only_used_in_recursion)]
fn validate_field_value_type_inner(
selection_set: &SelectionSet,
schema: &FederationSchema,
from_context_parent: &FieldArgumentDefinitionPosition,
errors: &mut MultipleFederationErrors,
) -> Option<Type> {
let mut types_array = Vec::new();
if selection_set.selections.is_empty() {
types_array.push(Type::Named(selection_set.ty.clone()));
}
for selection in selection_set.selections.iter() {
if let Selection::Field(field) = selection
&& let Some(nested_type) = validate_field_value_type_inner(
&field.selection_set,
schema,
from_context_parent,
errors,
)
{
types_array.push(nested_type);
}
}
if types_array.is_empty() {
return None;
}
types_array
.into_iter()
.map(Some)
.reduce(|acc, item| match (acc, item) {
(Some(acc), Some(item)) => {
if acc == item {
Some(acc)
} else if acc.is_assignable_to(&item) {
Some(item)
} else if item.is_assignable_to(&acc) {
Some(acc)
} else {
None
}
}
_ => None,
})
.flatten()
}
#[allow(dead_code)]
fn validate_field_value_type(
context: &str,
current_type: &TypeDefinitionPosition,
selection_set: &SelectionSet,
schema: &FederationSchema,
from_context_parent: &FieldArgumentDefinitionPosition,
errors: &mut MultipleFederationErrors,
) -> Result<Option<Type>, FederationError> {
if let Some(metadata) = &schema.subgraph_metadata
&& let Some(interface_object_directive) = metadata
.federation_spec_definition()
.interface_object_directive_definition(schema)?
&& current_type.has_applied_directive(schema, &interface_object_directive.name)
{
errors.push(
SingleFederationError::ContextSelectionInvalid {
message: format!("Context \"{}\" is used in \"{}\" but the selection is invalid: One of the types in the selection is an interfaceObject: \"{}\".", context, from_context_parent, current_type.type_name())
}
.into(),
);
}
Ok(validate_field_value_type_inner(
selection_set,
schema,
from_context_parent,
errors,
))
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use super::*;
use crate::error::MultipleFederationErrors;
use crate::error::SingleFederationError;
use crate::subgraph::test_utils::build_and_expand;
#[test]
fn test_deny_on_abstract_type() {
let schema_str = r#"
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.8", import: ["@context", "@fromContext"])
type Query {
test: String
}
interface Entity {
id(contextArg: ID! @fromContext(field: "$userContext userId")): ID!
}
"#;
let subgraph = build_and_expand(schema_str);
let mut errors = MultipleFederationErrors::new();
let context_map = HashMap::new();
validate_from_context_directives(
subgraph.schema(),
subgraph.metadata(),
&context_map,
&mut errors,
)
.expect("validates fromContext directives");
assert!(
errors.errors.iter().any(|e| matches!(
e,
SingleFederationError::ContextNotSet { message } if message == "@fromContext argument cannot be used on a field that exists on an abstract type \"Entity.id(contextArg:)\"."
)),
"Expected an error about abstract type"
);
}
#[test]
fn test_deny_on_interface_implementation() {
let schema_str = r#"
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.8", import: ["@context", "@fromContext"])
type Query {
user: User
}
interface Entity {
id: ID!
}
type User implements Entity {
id(contextArg: ID! @fromContext(field: "$userContext userId")): ID!
name: String
}
type UserContext @context(name: "userContext") {
userId: ID!
}
"#;
let subgraph = build_and_expand(schema_str);
let mut errors = MultipleFederationErrors::new();
let context_map = crate::schema::validators::context::validate_context_directives(
subgraph.schema(),
&mut errors,
)
.expect("validates context directives");
validate_from_context_directives(
subgraph.schema(),
subgraph.metadata(),
&context_map,
&mut errors,
)
.expect("validates fromContext directives");
assert!(
!errors.errors.is_empty(),
"Expected errors for @fromContext on interface implementation, but got none"
);
assert!(
errors.errors.iter().any(|e| matches!(
e,
SingleFederationError::ContextNotSet { message } if message == "@fromContext argument cannot be used on a field implementing an interface field \"Entity.id\"."
)),
"Expected an error about implementing an interface field, got: {:?}",
errors.errors
);
}
#[test]
fn test_require_context_exists() {
let schema_str = r#"
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.8", import: ["@context", "@fromContext"])
type Query {
user(id: ID! @fromContext(field: "$userContext id")): User
invalid(id: ID! @fromContext(field: "$invalidContext id")): User
noContext(id: ID! @fromContext(field: "$noSelection")): User
}
type User @context(name: "userContext") @context(name: "noSelection") {
id: ID!
name: String
}
"#;
let subgraph = build_and_expand(schema_str);
let mut errors = MultipleFederationErrors::new();
let context_map = crate::schema::validators::context::validate_context_directives(
subgraph.schema(),
&mut errors,
)
.expect("validates context directives");
validate_from_context_directives(
subgraph.schema(),
subgraph.metadata(),
&context_map,
&mut errors,
)
.expect("validates fromContext directives");
assert!(
!errors.errors.is_empty(),
"Expected validation errors for invalid context and missing selection"
);
assert!(
errors.errors.iter().any(|e| matches!(
e,
SingleFederationError::ContextNotSet { message } if message.contains("invalidContext") && message.contains("is never set")
)),
"Expected error for context that is never set, got: {:?}",
errors.errors
);
assert!(
errors.errors.iter().any(|e| matches!(
e,
SingleFederationError::NoSelectionForContext { message } if message.contains("noSelection") || message.contains("has no selection")
)),
"Expected error for missing selection, got: {:?}",
errors.errors
);
}
#[test]
fn test_require_resolvable_key() {
let schema_str = r#"
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.8", import: ["@context", "@fromContext", "@key"])
type Query {
user(id: ID! @fromContext(field: "$userContext { userId }")): User
}
type User @context(name: "userContext") @key(fields: "id", resolvable: false) {
id: ID!
name: String
}
"#;
let subgraph = build_and_expand(schema_str);
let mut errors = MultipleFederationErrors::new();
let context_map = crate::schema::validators::context::validate_context_directives(
subgraph.schema(),
&mut errors,
)
.expect("validates context directives");
validate_from_context_directives(
subgraph.schema(),
subgraph.metadata(),
&context_map,
&mut errors,
)
.expect("validates fromContext directives");
let resolvable_key_error = errors
.errors
.iter()
.find(|e| matches!(e, SingleFederationError::ContextNoResolvableKey { .. }));
if let Some(error) = resolvable_key_error {
assert!(
matches!(
error,
SingleFederationError::ContextNoResolvableKey { message } if message == "Object \"Query\" has no resolvable key but has a field with a contextual argument."
),
"Expected an error about no resolvable key"
);
}
}
#[test]
fn test_parse_context() {
let fields = [
("$context { prop }", ("context", "{ prop }")),
(
"$context ... on A { prop } ... on B { prop }",
("context", "... on A { prop } ... on B { prop }"),
),
(
"$topLevelQuery { me { locale } }",
("topLevelQuery", "{ me { locale } }"),
),
(
"$context { a { b { c { prop }}} }",
("context", "{ a { b { c { prop }}} }"),
),
(
"$ctx { identifiers { legacyUserId } }",
("ctx", "{ identifiers { legacyUserId } }"),
),
(
"$retailCtx { identifiers { id5 } }",
("retailCtx", "{ identifiers { id5 } }"),
),
("$retailCtx { mid }", ("retailCtx", "{ mid }")),
(
"$widCtx { identifiers { wid } }",
("widCtx", "{ identifiers { wid } }"),
),
];
for (field, (known_context, known_selection)) in fields {
let (context, selection) = parse_context(field);
assert_eq!(context, Some(known_context.to_string()));
assert_eq!(selection, Some(known_selection.to_string()));
}
assert_eq!(
parse_context("#comment $fakeContext fakeSelection"),
(None, Some("".to_string()))
);
assert_eq!(
parse_context("$ #comment fakeContext fakeSelection"),
(None, None)
);
let (parsed_context, parsed_selection) = parse_context("$contextA userId");
assert_eq!(parsed_context, Some("contextA".to_string()));
assert_eq!(parsed_selection, Some("userId".to_string()));
let (parsed_context, parsed_selection) = parse_context("invalidFormat");
assert_eq!(parsed_context, None);
assert_eq!(parsed_selection, Some("invalidFormat".to_string()));
let (parsed_context, parsed_selection) = parse_context("$ selection");
assert_eq!(parsed_context, Some("selection".to_string()));
assert_eq!(parsed_selection, None);
let (parsed_context, parsed_selection) = parse_context("$context ");
assert_eq!(parsed_context, Some("context".to_string()));
assert_eq!(parsed_selection, None);
let (parsed_context, parsed_selection) =
parse_context("$contextA multiple fields selected");
assert_eq!(parsed_context, Some("contextA".to_string()));
assert_eq!(
parsed_selection,
Some("multiple fields selected".to_string())
);
}
#[test]
fn test_validate_field_value_type_single_field() {
use crate::schema::position::FieldArgumentDefinitionPosition;
use crate::schema::position::ObjectFieldArgumentDefinitionPosition;
use crate::schema::position::ObjectTypeDefinitionPosition;
let schema_str = r#"
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.8", import: ["@context", "@fromContext"])
type Query {
contextual(id: ID): User
}
type User {
id: ID
name: String
age: Int
email: String
}
"#;
let subgraph = build_and_expand(schema_str);
let mut errors = MultipleFederationErrors::new();
let user_type = TypeDefinitionPosition::Object(ObjectTypeDefinitionPosition::new(
Name::new_unchecked("User"),
));
let query_contextual_arg_pos =
FieldArgumentDefinitionPosition::Object(ObjectFieldArgumentDefinitionPosition {
type_name: Name::new_unchecked("Query"),
field_name: Name::new_unchecked("contextual"),
argument_name: Name::new_unchecked("id"),
});
let fields = FieldSet::parse(
Valid::assume_valid_ref(subgraph.schema().schema()),
user_type.type_name().clone(),
"id",
"from_context.graphql",
)
.expect("valid field set");
let result = validate_field_value_type(
"userContext",
&user_type,
&fields.selection_set,
subgraph.schema(),
&query_contextual_arg_pos,
&mut errors,
)
.expect("valid field value type");
assert!(
result.is_some(),
"Should return a type for single field selection"
);
assert_eq!(
result.unwrap().inner_named_type().as_str(),
"ID",
"Should return ID type"
);
assert!(
errors.errors.is_empty(),
"Should not have validation errors"
);
}
#[test]
fn test_validate_field_value_type_consistent_fields() {
use crate::schema::position::FieldArgumentDefinitionPosition;
use crate::schema::position::ObjectFieldArgumentDefinitionPosition;
use crate::schema::position::ObjectTypeDefinitionPosition;
let schema_str = r#"
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.8", import: ["@context", "@fromContext"])
type Query {
contextual(id: ID): User
}
type User {
id: ID
userId: ID
identifier: ID
}
"#;
let subgraph = build_and_expand(schema_str);
let mut errors = MultipleFederationErrors::new();
let user_type = TypeDefinitionPosition::Object(ObjectTypeDefinitionPosition::new(
Name::new_unchecked("User"),
));
let query_contextual_arg_pos =
FieldArgumentDefinitionPosition::Object(ObjectFieldArgumentDefinitionPosition {
type_name: Name::new_unchecked("Query"),
field_name: Name::new_unchecked("contextual"),
argument_name: Name::new_unchecked("id"),
});
let fields = FieldSet::parse(
Valid::assume_valid_ref(subgraph.schema().schema()),
user_type.type_name().clone(),
"{ id userId identifier }",
"from_context.graphql",
)
.expect("valid field set");
let result = validate_field_value_type(
"userContext",
&user_type,
&fields.selection_set,
subgraph.schema(),
&query_contextual_arg_pos,
&mut errors,
)
.expect("valid field value type");
assert!(
result.is_some(),
"Should return a type for consistent field types"
);
assert_eq!(
result.unwrap().inner_named_type().as_str(),
"ID",
"Should return common ID type"
);
assert!(
errors.errors.is_empty(),
"Should not have validation errors"
);
}
#[test]
fn test_validate_field_value_type_inconsistent_fields() {
use crate::schema::position::FieldArgumentDefinitionPosition;
use crate::schema::position::ObjectFieldArgumentDefinitionPosition;
use crate::schema::position::ObjectTypeDefinitionPosition;
let schema_str = r#"
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.8", import: ["@context", "@fromContext"])
type Query {
contextual(id: ID): User
}
type User {
id: ID
name: String
age: Int
}
"#;
let subgraph = build_and_expand(schema_str);
let mut errors = MultipleFederationErrors::new();
let user_type = TypeDefinitionPosition::Object(ObjectTypeDefinitionPosition::new(
Name::new_unchecked("User"),
));
let query_contextual_arg_pos =
FieldArgumentDefinitionPosition::Object(ObjectFieldArgumentDefinitionPosition {
type_name: Name::new_unchecked("Query"),
field_name: Name::new_unchecked("contextual"),
argument_name: Name::new_unchecked("id"),
});
let fields = FieldSet::parse(
Valid::assume_valid_ref(subgraph.schema().schema()),
user_type.type_name().clone(),
"{ id name age }",
"from_context.graphql",
)
.expect("valid field set");
let result = validate_field_value_type(
"userContext",
&user_type,
&fields.selection_set,
subgraph.schema(),
&query_contextual_arg_pos,
&mut errors,
)
.expect("valid field value type");
assert!(
result.is_none(),
"Should return None for inconsistent field types"
);
assert!(
errors.errors.is_empty(),
"Should not have validation errors for type mismatch"
);
}
#[test]
fn test_validate_field_value_type_nested_selection() {
use crate::schema::position::FieldArgumentDefinitionPosition;
use crate::schema::position::ObjectFieldArgumentDefinitionPosition;
use crate::schema::position::ObjectTypeDefinitionPosition;
let schema_str = r#"
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.8", import: ["@context", "@fromContext"])
type Query {
contextual(id: ID): User
}
type User {
profile: Profile
settings: Profile
}
type Profile {
id: ID
name: String
}
"#;
let subgraph = build_and_expand(schema_str);
let mut errors = MultipleFederationErrors::new();
let user_type = TypeDefinitionPosition::Object(ObjectTypeDefinitionPosition::new(
Name::new_unchecked("User"),
));
let query_contextual_arg_pos =
FieldArgumentDefinitionPosition::Object(ObjectFieldArgumentDefinitionPosition {
type_name: Name::new_unchecked("Query"),
field_name: Name::new_unchecked("contextual"),
argument_name: Name::new_unchecked("id"),
});
let fields = FieldSet::parse(
Valid::assume_valid_ref(subgraph.schema().schema()),
user_type.type_name().clone(),
"{ profile { id } settings { id } }",
"from_context.graphql",
)
.expect("valid field set");
let result = validate_field_value_type(
"userContext",
&user_type,
&fields.selection_set,
subgraph.schema(),
&query_contextual_arg_pos,
&mut errors,
)
.expect("valid field value type");
assert!(
result.is_some(),
"Should return a type for nested consistent selections"
);
assert_eq!(
result.unwrap().inner_named_type().as_str(),
"ID",
"Should return ID type from nested selection"
);
assert!(
errors.errors.is_empty(),
"Should not have validation errors"
);
}
#[test]
fn test_validate_field_value_type_nested_inconsistent() {
use crate::schema::position::FieldArgumentDefinitionPosition;
use crate::schema::position::ObjectFieldArgumentDefinitionPosition;
use crate::schema::position::ObjectTypeDefinitionPosition;
let schema_str = r#"
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.8", import: ["@context", "@fromContext"])
type Query {
contextual(id: ID): User
}
type User {
profile: Profile
settings: Settings
}
type Profile {
id: ID
}
type Settings {
name: String
}
"#;
let subgraph = build_and_expand(schema_str);
let mut errors = MultipleFederationErrors::new();
let user_type = TypeDefinitionPosition::Object(ObjectTypeDefinitionPosition::new(
Name::new_unchecked("User"),
));
let query_contextual_arg_pos =
FieldArgumentDefinitionPosition::Object(ObjectFieldArgumentDefinitionPosition {
type_name: Name::new_unchecked("Query"),
field_name: Name::new_unchecked("contextual"),
argument_name: Name::new_unchecked("id"),
});
let fields = FieldSet::parse(
Valid::assume_valid_ref(subgraph.schema().schema()),
user_type.type_name().clone(),
"{ profile { id } settings { name } }",
"from_context.graphql",
)
.expect("valid field set");
let result = validate_field_value_type(
"userContext",
&user_type,
&fields.selection_set,
subgraph.schema(),
&query_contextual_arg_pos,
&mut errors,
)
.expect("valid field value type");
assert!(
result.is_none(),
"Should return None for nested inconsistent selections"
);
assert!(
errors.errors.is_empty(),
"Should not have validation errors for type mismatch"
);
}
#[test]
fn test_validate_field_value_type_interface_object_error() {
use crate::schema::position::FieldArgumentDefinitionPosition;
use crate::schema::position::ObjectFieldArgumentDefinitionPosition;
use crate::schema::position::ObjectTypeDefinitionPosition;
let schema_str = r#"
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.8", import: ["@context", "@fromContext", "@interfaceObject"])
type Query {
contextual(id: ID): User
}
type User @interfaceObject {
id: ID
name: String
}
"#;
let subgraph = build_and_expand(schema_str);
let mut errors = MultipleFederationErrors::new();
let user_type = TypeDefinitionPosition::Object(ObjectTypeDefinitionPosition::new(
Name::new_unchecked("User"),
));
let query_contextual_arg_pos =
FieldArgumentDefinitionPosition::Object(ObjectFieldArgumentDefinitionPosition {
type_name: Name::new_unchecked("Query"),
field_name: Name::new_unchecked("contextual"),
argument_name: Name::new_unchecked("id"),
});
let fields = FieldSet::parse(
Valid::assume_valid_ref(subgraph.schema().schema()),
user_type.type_name().clone(),
"{ id }",
"from_context.graphql",
)
.expect("valid field set");
let result = validate_field_value_type(
"userContext",
&user_type,
&fields.selection_set,
subgraph.schema(),
&query_contextual_arg_pos,
&mut errors,
)
.expect("valid field value type");
assert!(
result.is_some(),
"Should still return a type even with interface object error"
);
assert!(
!errors.errors.is_empty(),
"Should have validation error for interface object"
);
assert!(
errors.errors.iter().any(|e| matches!(
e,
SingleFederationError::ContextSelectionInvalid { message } if message == "Context \"userContext\" is used in \"Query.contextual(id:)\" but the selection is invalid: One of the types in the selection is an interfaceObject: \"User\"."
)),
"Should have specific interface object error"
);
}
#[test]
fn test_validate_field_value_type_wrapped_types() {
use crate::schema::position::FieldArgumentDefinitionPosition;
use crate::schema::position::ObjectFieldArgumentDefinitionPosition;
use crate::schema::position::ObjectTypeDefinitionPosition;
let schema_str = r#"
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.8", import: ["@context", "@fromContext"])
type Query {
contextual(id: ID): User
}
type User {
id: ID
idNonNull: ID!
ids: [ID]
idsNonNull: [ID!]!
idsNonNullList: [ID!]!
}
"#;
let subgraph = build_and_expand(schema_str);
let mut errors = MultipleFederationErrors::new();
let user_type = TypeDefinitionPosition::Object(ObjectTypeDefinitionPosition::new(
Name::new_unchecked("User"),
));
let query_contextual_arg_pos =
FieldArgumentDefinitionPosition::Object(ObjectFieldArgumentDefinitionPosition {
type_name: Name::new_unchecked("Query"),
field_name: Name::new_unchecked("contextual"),
argument_name: Name::new_unchecked("id"),
});
let fields = FieldSet::parse(
Valid::assume_valid_ref(subgraph.schema().schema()),
user_type.type_name().clone(),
"{ id idNonNull ids idsNonNull }",
"from_context.graphql",
)
.expect("valid field set");
let result = validate_field_value_type(
"userContext",
&user_type,
&fields.selection_set,
subgraph.schema(),
&query_contextual_arg_pos,
&mut errors,
)
.expect("valid field value type");
assert!(
result.is_some(),
"Should return a type for wrapped types with same base"
);
assert_eq!(
result.unwrap().inner_named_type().as_str(),
"ID",
"Should return common base type ID"
);
assert!(
errors.errors.is_empty(),
"Should not have validation errors"
);
}
#[test]
fn test_validate_field_value_type_deep_nesting() {
use crate::schema::position::FieldArgumentDefinitionPosition;
use crate::schema::position::ObjectFieldArgumentDefinitionPosition;
use crate::schema::position::ObjectTypeDefinitionPosition;
let schema_str = r#"
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.8", import: ["@context", "@fromContext"])
type Query {
contextual(id: ID): User
}
type User {
profile: Profile
}
type Profile {
settings: Settings
}
type Settings {
id: ID
}
"#;
let subgraph = build_and_expand(schema_str);
let mut errors = MultipleFederationErrors::new();
let user_type = TypeDefinitionPosition::Object(ObjectTypeDefinitionPosition::new(
Name::new_unchecked("User"),
));
let query_contextual_arg_pos =
FieldArgumentDefinitionPosition::Object(ObjectFieldArgumentDefinitionPosition {
type_name: Name::new_unchecked("Query"),
field_name: Name::new_unchecked("contextual"),
argument_name: Name::new_unchecked("id"),
});
let fields = FieldSet::parse(
Valid::assume_valid_ref(subgraph.schema().schema()),
user_type.type_name().clone(),
"{ profile { settings { id } } }",
"from_context.graphql",
)
.expect("valid field set");
let result = validate_field_value_type(
"userContext",
&user_type,
&fields.selection_set,
subgraph.schema(),
&query_contextual_arg_pos,
&mut errors,
)
.expect("valid field value type");
assert!(
result.is_some(),
"Should return a type for deeply nested selection"
);
assert_eq!(
result.unwrap().inner_named_type().as_str(),
"ID",
"Should return the deeply nested field type"
);
assert!(
errors.errors.is_empty(),
"Should not have validation errors"
);
}
#[test]
fn test_validate_field_value_basic_success() {
let schema_str = r#"
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.8", import: ["@context", "@fromContext", "@key"])
type Query {
parent: Parent
}
type Parent @context(name: "userContext") @key(fields: "id") {
id: ID!
name: String
}
type Target @key(fields: "targetId") {
targetId: ID!
value(contextArg: String @fromContext(field: "$userContext name")): String
}
"#;
let subgraph = build_and_expand(schema_str);
let mut errors = MultipleFederationErrors::new();
let applied_directives = subgraph
.schema()
.from_context_directive_applications()
.expect("valid from context directive");
let applied_directive = applied_directives
.first()
.expect("at least one from context directive")
.as_ref()
.expect("valid from context directive");
let (context, selection) = parse_context(applied_directive.arguments.field);
let set_context_locations = vec![Name::new_unchecked("Parent")];
let result = validate_field_value(
&context.expect("valid context"),
&selection.expect("valid selection"),
applied_directive,
&set_context_locations,
subgraph.schema(),
&mut errors,
);
assert!(result.is_ok(), "Should validate successfully");
assert!(
errors.errors.is_empty(),
"Should not have validation errors"
);
}
#[test]
fn test_validate_field_value_invalid_selection() {
let schema_str = r#"
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.8", import: ["@context", "@fromContext", "@key"])
type Query {
parent: Parent
}
type Parent @context(name: "userContext") @key(fields: "id") {
id: ID!
name: String
}
type Target @key(fields: "targetId") {
targetId: ID!
value(contextArg: String! @fromContext(field: "$userContext nonExistentField")): String
}
"#;
let subgraph = build_and_expand(schema_str);
let mut errors = MultipleFederationErrors::new();
let set_context_locations = vec![Name::new_unchecked("Parent")];
let applied_directives = subgraph
.schema()
.from_context_directive_applications()
.expect("valid from context directive");
let applied_directive = applied_directives
.first()
.expect("at least one from context directive")
.as_ref()
.expect("valid from context directive");
let (context, selection) = parse_context(applied_directive.arguments.field);
let result = validate_field_value(
&context.expect("valid context"),
&selection.expect("valid selection"),
applied_directive,
&set_context_locations,
subgraph.schema(),
&mut errors,
);
assert!(result.is_ok(), "Function should complete");
assert!(
!errors.errors.is_empty(),
"Should have validation errors for invalid selection"
);
assert!(
errors.errors.iter().any(|e| matches!(
e,
SingleFederationError::ContextSelectionInvalid { message } if message == "Context \"userContext\" is used in \"Target.value(contextArg:)\" but the selection is invalid for type \"Parent\"."
)),
"Should have specific invalid selection error"
);
}
#[test]
fn test_validate_field_value_type_mismatch() {
let schema_str = r#"
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.8", import: ["@context", "@fromContext", "@key"])
type Query {
parent: Parent
}
type Parent @context(name: "userContext") @key(fields: "id") {
id: ID!
name: String
}
type Target @key(fields: "targetId") {
targetId: ID!
value(contextArg: ID! @fromContext(field: "$userContext name")): String
}
"#;
let subgraph = build_and_expand(schema_str);
let mut errors = MultipleFederationErrors::new();
let set_context_locations = vec![Name::new_unchecked("Parent")];
let applied_directives = subgraph
.schema()
.from_context_directive_applications()
.expect("valid from context directive");
let applied_directive = applied_directives
.first()
.expect("at least one from context directive")
.as_ref()
.expect("valid from context directive");
let (context, selection) = parse_context(applied_directive.arguments.field);
let result = validate_field_value(
&context.expect("valid context"),
&selection.expect("valid selection"),
applied_directive,
&set_context_locations,
subgraph.schema(),
&mut errors,
);
assert!(result.is_ok(), "Function should complete");
assert!(
!errors.errors.is_empty(),
"Should have validation errors for type mismatch"
);
assert!(
errors.errors.iter().any(|e| matches!(
e,
SingleFederationError::ContextSelectionInvalid { message } if message == "Context \"userContext\" is used in \"Target.value(contextArg:)\" but the selection is invalid: the type of the selection \"String\" does not match the expected type \"ID!\""
)),
"Should have specific type mismatch error"
);
}
#[test]
fn test_validate_field_value_inline_fragments() {
let schema_str = r#"
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.8", import: ["@context", "@fromContext", "@key"])
type Query {
parent: Parent
}
type Parent @context(name: "userContext") @key(fields: "id") {
id: ID!
name: String
}
type Target @key(fields: "targetId") {
targetId: ID!
value(contextArg: String! @fromContext(field: "$userContext ... on Parent { name }")): String
}
"#;
let subgraph = build_and_expand(schema_str);
let mut errors = MultipleFederationErrors::new();
let set_context_locations = vec![Name::new_unchecked("Parent")];
let applied_directives = subgraph
.schema()
.from_context_directive_applications()
.expect("valid from context directive");
let applied_directive = applied_directives
.first()
.expect("at least one from context directive")
.as_ref()
.expect("valid from context directive");
let (context, selection) = parse_context(applied_directive.arguments.field);
let result = validate_field_value(
&context.expect("valid context"),
&selection.expect("valid selection"),
applied_directive,
&set_context_locations,
subgraph.schema(),
&mut errors,
);
assert!(result.is_ok(), "Should handle inline fragments");
}
#[test]
fn test_validate_field_value_type_conditions_same_name() {
let schema_str = r#"
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.8", import: ["@context", "@fromContext", "@key"])
type Query {
parent: Parent
}
type Parent @context(name: "userContext") @key(fields: "id") {
id: ID!
name: String
}
type Target @key(fields: "targetId") {
targetId: ID!
value(contextArg: String! @fromContext(field: "$userContext ... on Parent { name } ... on Parent { name }")): String
}
"#;
let subgraph = build_and_expand(schema_str);
let mut errors = MultipleFederationErrors::new();
let set_context_locations = vec![Name::new_unchecked("Parent")];
let applied_directives = subgraph
.schema()
.from_context_directive_applications()
.expect("valid from context directive");
let applied_directive = applied_directives
.first()
.expect("at least one from context directive")
.as_ref()
.expect("valid from context directive");
let (context, selection) = parse_context(applied_directive.arguments.field);
let result = validate_field_value(
&context.expect("valid context"),
&selection.expect("valid selection"),
applied_directive,
&set_context_locations,
subgraph.schema(),
&mut errors,
);
assert!(result.is_ok(), "Should handle inline fragments");
assert!(!errors.errors.is_empty(), "Should have validation error");
assert!(
errors.errors.iter().any(|e| matches!(
e,
SingleFederationError::ContextSelectionInvalid { message } if message == "Context \"userContext\" is used in \"Target.value(contextArg:)\" but the selection is invalid: type conditions have the same name"
)),
"Should have specific type conditions same name error"
);
}
#[test]
fn test_validate_field_value_type_mismatch_multiple_contexts() {
let schema_str = r#"
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.8", import: ["@context", "@fromContext", "@key"])
type Query {
foo: Foo
bar: Bar
}
type Foo @context(name: "context") @key(fields: "id") {
id: ID!
prop: String!
}
type Bar @context(name: "context") @key(fields: "id") {
id: ID!
prop: Int!
}
type Target @key(fields: "targetId") {
targetId: ID!
value(contextArg: String @fromContext(field: "$context { prop }")): String
}
"#;
let subgraph = build_and_expand(schema_str);
let mut errors = MultipleFederationErrors::new();
let set_context_locations = vec![Name::new_unchecked("Foo"), Name::new_unchecked("Bar")];
let applied_directives = subgraph
.schema()
.from_context_directive_applications()
.expect("valid from context directive");
let applied_directive = applied_directives
.first()
.expect("at least one from context directive")
.as_ref()
.expect("valid from context directive");
let (context, selection) = parse_context(applied_directive.arguments.field);
let result = validate_field_value(
&context.expect("valid context"),
&selection.expect("valid selection"),
applied_directive,
&set_context_locations,
subgraph.schema(),
&mut errors,
);
assert!(result.is_ok(), "Function should complete");
assert!(
!errors.errors.is_empty(),
"Should have validation errors for type mismatch"
);
assert!(
errors.errors.iter().any(|e| matches!(
e,
SingleFederationError::ContextSelectionInvalid { message } if message == "Context \"context\" is used in \"Target.value(contextArg:)\" but the selection is invalid: the type of the selection \"Int\" does not match the expected type \"String\""
)),
"Should have specific type mismatch error"
);
}
#[test]
fn test_validate_field_value_no_context_reference() {
let schema_str = r#"
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.8", import: ["@context", "@fromContext", "@key"])
type Query {
parent: Parent
}
type Parent @context(name: "context") @key(fields: "id") {
id: ID!
prop: String!
}
type Target @key(fields: "targetId") {
targetId: ID!
value(contextArg: String! @fromContext(field: "prop")): String
}
"#;
let subgraph = build_and_expand(schema_str);
let mut errors = MultipleFederationErrors::new();
let context_map = crate::schema::validators::context::validate_context_directives(
subgraph.schema(),
&mut errors,
)
.expect("validates context directives");
validate_from_context_directives(
subgraph.schema(),
subgraph.metadata(),
&context_map,
&mut errors,
)
.expect("validates fromContext directives");
assert!(
!errors.errors.is_empty(),
"Expected validation errors for missing context reference"
);
assert!(
errors.errors.iter().any(|e| matches!(
e,
SingleFederationError::NoContextReferenced { message } if message.contains("does not reference a context")
)),
"Expected error for no context reference (missing $ sign), got: {:?}",
errors.errors
);
}
#[test]
fn test_validate_field_value_multiple_selections() {
let schema_str = r#"
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.8", import: ["@context", "@fromContext", "@key"])
type Query {
parent: Parent
}
type Parent @context(name: "context") @key(fields: "id") {
id: ID!
prop: String!
name: String!
}
type Target @key(fields: "targetId") {
targetId: ID!
value(contextArg: String! @fromContext(field: "$context { id prop }")): String
}
"#;
let subgraph = build_and_expand(schema_str);
let mut errors = MultipleFederationErrors::new();
let set_context_locations = vec![Name::new_unchecked("Parent")];
let applied_directives = subgraph
.schema()
.from_context_directive_applications()
.expect("valid from context directive");
let applied_directive = applied_directives
.first()
.expect("at least one from context directive")
.as_ref()
.expect("valid from context directive");
let (context, selection) = parse_context(applied_directive.arguments.field);
let result = validate_field_value(
&context.expect("valid context"),
&selection.expect("valid selection"),
applied_directive,
&set_context_locations,
subgraph.schema(),
&mut errors,
);
assert!(result.is_ok(), "Function should complete");
assert!(
!errors.errors.is_empty(),
"Should have validation errors for multiple selections"
);
assert!(
errors.errors.iter().any(|e| matches!(
e,
SingleFederationError::ContextSelectionInvalid { message } if message == "Context \"context\" is used in \"Target.value(contextArg:)\" but the selection is invalid: multiple selections are made"
)),
"Should have specific multiple selections error"
);
}
#[test]
fn test_validate_field_value_with_directives() {
let schema_str = r#"
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.8", import: ["@context", "@fromContext", "@key"])
directive @testDirective on FIELD
type Query {
parent: Parent
}
type Parent @context(name: "context") @key(fields: "id") {
id: ID!
prop: String!
}
type Target @key(fields: "targetId") {
targetId: ID!
value(contextArg: String! @fromContext(field: "$context { prop @testDirective }")): String
}
"#;
let subgraph = build_and_expand(schema_str);
let mut errors = MultipleFederationErrors::new();
let set_context_locations = vec![Name::new_unchecked("Parent")];
let applied_directives = subgraph
.schema()
.from_context_directive_applications()
.expect("valid from context directive");
let applied_directive = applied_directives
.first()
.expect("at least one from context directive")
.as_ref()
.expect("valid from context directive");
let (context, selection) = parse_context(applied_directive.arguments.field);
let result = validate_field_value(
&context.expect("valid context"),
&selection.expect("valid selection"),
applied_directive,
&set_context_locations,
subgraph.schema(),
&mut errors,
);
assert!(result.is_ok(), "Function should complete");
assert!(
!errors.errors.is_empty(),
"Should have validation errors for directives"
);
assert!(
errors.errors.iter().any(|e| matches!(
e,
SingleFederationError::ContextSelectionInvalid { message } if message == "Context \"context\" is used in \"Target.value(contextArg:)\" but the selection is invalid: directives are not allowed in the selection"
)),
"Should have specific directive error"
);
}
#[test]
fn test_validate_field_value_with_aliases() {
let schema_str = r#"
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.8", import: ["@context", "@fromContext", "@key"])
type Query {
parent: Parent
}
type Parent @context(name: "context") @key(fields: "id") {
id: ID!
prop: String!
}
type Target @key(fields: "targetId") {
targetId: ID!
value(contextArg: String! @fromContext(field: "$context { alias: prop }")): String
}
"#;
let subgraph = build_and_expand(schema_str);
let mut errors = MultipleFederationErrors::new();
let set_context_locations = vec![Name::new_unchecked("Parent")];
let applied_directives = subgraph
.schema()
.from_context_directive_applications()
.expect("valid from context directive");
let applied_directive = applied_directives
.first()
.expect("at least one from context directive")
.as_ref()
.expect("valid from context directive");
let (context, selection) = parse_context(applied_directive.arguments.field);
let result = validate_field_value(
&context.expect("valid context"),
&selection.expect("valid selection"),
applied_directive,
&set_context_locations,
subgraph.schema(),
&mut errors,
);
assert!(result.is_ok(), "Function should complete");
assert!(
!errors.errors.is_empty(),
"Should have validation errors for aliases"
);
assert!(
errors.errors.iter().any(|e| matches!(
e,
SingleFederationError::ContextSelectionInvalid { message } if message == "Context \"context\" is used in \"Target.value(contextArg:)\" but the selection is invalid: aliases are not allowed in the selection"
)),
"Should have specific alias error"
);
}
#[test]
fn test_validate_field_value_type_condition_no_match() {
let schema_str = r#"
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.8", import: ["@context", "@fromContext", "@key"])
type Query {
bar: Bar
}
type Foo @key(fields: "id") {
id: ID!
prop: String!
}
type Bar @context(name: "context") @key(fields: "id") {
id: ID!
prop2: String!
}
type Target @key(fields: "targetId") {
targetId: ID!
value(contextArg: String! @fromContext(field: "$context ... on Foo { prop }")): String
}
"#;
let subgraph = build_and_expand(schema_str);
let mut errors = MultipleFederationErrors::new();
let set_context_locations = vec![Name::new_unchecked("Bar")];
let applied_directives = subgraph
.schema()
.from_context_directive_applications()
.expect("valid from context directive");
let applied_directive = applied_directives
.first()
.expect("at least one from context directive")
.as_ref()
.expect("valid from context directive");
let (context, selection) = parse_context(applied_directive.arguments.field);
let result = validate_field_value(
&context.expect("valid context"),
&selection.expect("valid selection"),
applied_directive,
&set_context_locations,
subgraph.schema(),
&mut errors,
);
assert!(result.is_ok(), "Function should complete");
assert!(
!errors.errors.is_empty(),
"Should have validation errors for type condition mismatch"
);
assert!(
errors.errors.iter().any(|e| matches!(
e,
SingleFederationError::ContextSelectionInvalid { message } if message == "Context \"context\" is used in \"Target.value(contextArg:)\" but the selection is invalid: no type condition matches the location \"Bar\""
)),
"Should have specific type condition mismatch error"
);
}
#[test]
fn test_deny_fromcontext_on_interface_field() {
let schema_str = r#"
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.8", import: ["@context", "@fromContext", "@key"])
type Query {
test: String
}
interface Entity {
id(contextArg: ID! @fromContext(field: "$userContext userId")): ID!
}
type UserContext @context(name: "userContext") {
userId: ID!
}
"#;
let subgraph = build_and_expand(schema_str);
let mut errors = MultipleFederationErrors::new();
let context_map = crate::schema::validators::context::validate_context_directives(
subgraph.schema(),
&mut errors,
)
.expect("validates context directives");
validate_from_context_directives(
subgraph.schema(),
subgraph.metadata(),
&context_map,
&mut errors,
)
.expect("validates fromContext directives");
assert!(!errors.errors.is_empty(), "Should have validation errors");
assert!(
errors.errors.iter().any(|e| matches!(
e,
SingleFederationError::ContextNotSet { message } if message == "@fromContext argument cannot be used on a field that exists on an abstract type \"Entity.id(contextArg:)\"."
)),
"Expected an error about abstract type"
);
}
#[test]
fn test_empty_context_name() {
let schema_str = r#"
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.8", import: ["@context", "@fromContext", "@key"])
type Query {
test: String
}
type TestType @context(name: "") @key(fields: "id") {
id: ID!
prop: String!
}
"#;
let subgraph = build_and_expand(schema_str);
let mut errors = MultipleFederationErrors::new();
let _context_map = crate::schema::validators::context::validate_context_directives(
subgraph.schema(),
&mut errors,
);
}
#[test]
fn test_context_fails_on_union_missing_prop() {
let schema_str = r#"
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.8", import: ["@context", "@fromContext", "@key"])
type Query {
t: T
}
union T @context(name: "context") = T1 | T2
type T1 @key(fields: "id") @context(name: "context") {
id: ID!
prop: String!
a: String!
}
type T2 @key(fields: "id") @context(name: "context") {
id: ID!
b: String!
}
type Target @key(fields: "targetId") {
targetId: ID!
value(contextArg: String @fromContext(field: "$context { prop }")): String
}
"#;
let subgraph = build_and_expand(schema_str);
let mut errors = MultipleFederationErrors::new();
let set_context_locations = vec![Name::new_unchecked("T")];
let applied_directives = subgraph
.schema()
.from_context_directive_applications()
.expect("valid from context directive");
let applied_directive = applied_directives
.first()
.expect("at least one from context directive")
.as_ref()
.expect("valid from context directive");
let (context, selection) = parse_context(applied_directive.arguments.field);
let result = validate_field_value(
&context.expect("valid context"),
&selection.expect("valid selection"),
applied_directive,
&set_context_locations,
subgraph.schema(),
&mut errors,
);
assert!(result.is_ok(), "Function should complete");
assert!(
!errors.errors.is_empty(),
"Should have validation errors for missing field in union member"
);
assert!(
errors.errors.iter().any(|e| matches!(
e,
SingleFederationError::ContextSelectionInvalid { message } if message == "Context \"context\" is used in \"Target.value(contextArg:)\" but the selection is invalid for type \"T\"."
)),
"Should have specific union field error"
);
}
#[test]
fn test_context_name_with_underscore() {
let schema_str = r#"
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.8", import: ["@context", "@fromContext", "@key"])
type Query {
parent: Parent
}
type Parent @context(name: "_context") @key(fields: "id") {
id: ID!
prop: String!
}
type Target @key(fields: "targetId") {
targetId: ID!
value(contextArg: String! @fromContext(field: "$_context prop")): String
}
"#;
let subgraph = build_and_expand(schema_str);
let mut errors = MultipleFederationErrors::new();
let context_map = crate::schema::validators::context::validate_context_directives(
subgraph.schema(),
&mut errors,
)
.expect("validates context directives");
validate_from_context_directives(
subgraph.schema(),
subgraph.metadata(),
&context_map,
&mut errors,
)
.expect("validates fromContext directives");
assert!(
!errors.errors.is_empty(),
"Should have validation errors for underscore in context name"
);
}
#[test]
fn test_forbid_default_values_on_contextual_arguments() {
let schema_str = r#"
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.8", import: ["@context", "@fromContext", "@key"])
type Query {
parent: Parent
}
type Parent @context(name: "context") @key(fields: "id") {
id: ID!
prop: String!
}
type Target @key(fields: "targetId") {
targetId: ID!
value(contextArg: String = "default" @fromContext(field: "$context prop")): String
}
"#;
let subgraph = build_and_expand(schema_str);
let mut errors = MultipleFederationErrors::new();
let context_map = crate::schema::validators::context::validate_context_directives(
subgraph.schema(),
&mut errors,
)
.expect("validates context directives");
validate_from_context_directives(
subgraph.schema(),
subgraph.metadata(),
&context_map,
&mut errors,
)
.expect("validates fromContext directives");
assert!(
!errors.errors.is_empty(),
"Should have validation errors for default values on contextual arguments"
);
}
#[test]
fn test_vanilla_setcontext_success() {
let schema_str = r#"
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.8", import: ["@context", "@fromContext", "@key"])
type Query {
t: T!
}
type T @key(fields: "id") @context(name: "context") {
id: ID!
u: U!
prop: String!
}
type U @key(fields: "id") {
id: ID!
field(a: String @fromContext(field: "$context { prop }")): Int!
}
"#;
let subgraph = build_and_expand(schema_str);
let mut errors = MultipleFederationErrors::new();
let context_map = crate::schema::validators::context::validate_context_directives(
subgraph.schema(),
&mut errors,
)
.expect("validates context directives");
validate_from_context_directives(
subgraph.schema(),
subgraph.metadata(),
&context_map,
&mut errors,
)
.expect("validates fromContext directives");
assert!(
errors.errors.is_empty(),
"Should not have validation errors for valid basic @fromContext usage"
);
}
#[test]
fn test_using_list_as_input_to_fromcontext() {
let schema_str = r#"
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.8", import: ["@context", "@fromContext", "@key"])
type Query {
t: T!
}
type T @key(fields: "id") @context(name: "context") {
id: ID!
u: U!
prop: [String]!
}
type U @key(fields: "id") {
id: ID!
field(a: [String] @fromContext(field: "$context { prop }")): Int!
}
"#;
let subgraph = build_and_expand(schema_str);
let mut errors = MultipleFederationErrors::new();
let context_map = crate::schema::validators::context::validate_context_directives(
subgraph.schema(),
&mut errors,
)
.expect("validates context directives");
validate_from_context_directives(
subgraph.schema(),
subgraph.metadata(),
&context_map,
&mut errors,
)
.expect("validates fromContext directives");
assert!(
errors.errors.is_empty(),
"Should not have validation errors for valid list type usage"
);
}
#[test]
fn test_set_context_multiple_contexts_duck_typing_success() {
let schema_str = r#"
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.8", import: ["@context", "@fromContext", "@key"])
type Query {
foo: Foo!
bar: Bar!
}
type Foo @key(fields: "id") @context(name: "context") {
id: ID!
u: U!
prop: String!
}
type Bar @key(fields: "id") @context(name: "context") {
id: ID!
u: U!
prop: String!
}
type U @key(fields: "id") {
id: ID!
field(a: String @fromContext(field: "$context { prop }")): Int!
}
"#;
let subgraph = build_and_expand(schema_str);
let mut errors = MultipleFederationErrors::new();
let context_map = crate::schema::validators::context::validate_context_directives(
subgraph.schema(),
&mut errors,
)
.expect("validates context directives");
validate_from_context_directives(
subgraph.schema(),
subgraph.metadata(),
&context_map,
&mut errors,
)
.expect("validates fromContext directives");
assert!(
errors.errors.is_empty(),
"Should not have validation errors for duck typing with same field types"
);
}
#[test]
fn test_set_context_multiple_contexts_type_conditions_success() {
let schema_str = r#"
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.8", import: ["@context", "@fromContext", "@key"])
type Query {
foo: Foo!
bar: Bar!
}
type Foo @key(fields: "id") @context(name: "context") {
id: ID!
u: U!
prop: String!
}
type Bar @key(fields: "id") @context(name: "context") {
id: ID!
u: U!
prop2: String!
}
type U @key(fields: "id") {
id: ID!
field(a: String @fromContext(field: "$context ... on Foo { prop } ... on Bar { prop2 }")): Int!
}
"#;
let subgraph = build_and_expand(schema_str);
let mut errors = MultipleFederationErrors::new();
let context_map = crate::schema::validators::context::validate_context_directives(
subgraph.schema(),
&mut errors,
)
.expect("validates context directives");
validate_from_context_directives(
subgraph.schema(),
subgraph.metadata(),
&context_map,
&mut errors,
)
.expect("validates fromContext directives");
assert!(
errors.errors.is_empty(),
"Should not have validation errors for valid interface context"
);
}
#[test]
fn test_set_context_on_interface_success() {
let schema_str = r#"
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.8", import: ["@context", "@fromContext", "@key"])
type Query {
i: I!
}
interface I @context(name: "context") {
prop: String!
}
type T implements I @key(fields: "id") {
id: ID!
u: U!
prop: String!
}
type U @key(fields: "id") {
id: ID!
field(a: String @fromContext(field: "$context { prop }")): Int!
}
"#;
let subgraph = build_and_expand(schema_str);
let mut errors = MultipleFederationErrors::new();
let context_map = crate::schema::validators::context::validate_context_directives(
subgraph.schema(),
&mut errors,
)
.expect("validates context directives");
validate_from_context_directives(
subgraph.schema(),
subgraph.metadata(),
&context_map,
&mut errors,
)
.expect("validates fromContext directives");
assert!(
errors.errors.is_empty(),
"Should not have validation errors for valid interface context"
);
}
#[test]
fn test_set_context_on_interface_with_type_condition_success() {
let schema_str = r#"
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.8", import: ["@context", "@fromContext", "@key"])
type Query {
i: I!
}
interface I @context(name: "context") {
prop: String!
}
type T implements I @key(fields: "id") {
id: ID!
u: U!
prop: String!
}
type U @key(fields: "id") {
id: ID!
field(a: String @fromContext(field: "$context ... on T { prop }")): Int!
}
"#;
let subgraph = build_and_expand(schema_str);
let mut errors = MultipleFederationErrors::new();
let context_map = crate::schema::validators::context::validate_context_directives(
subgraph.schema(),
&mut errors,
)
.expect("validates context directives");
validate_from_context_directives(
subgraph.schema(),
subgraph.metadata(),
&context_map,
&mut errors,
)
.expect("validates fromContext directives");
assert!(
errors.errors.is_empty(),
"Should not have validation errors for valid interface context"
);
}
#[test]
fn test_nullability_mismatch_ok_if_contextual_value_non_nullable() {
let schema_str = r#"
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.8", import: ["@context", "@fromContext", "@key"])
type Query {
t: T!
}
type T @key(fields: "id") @context(name: "context") {
id: ID!
u: U!
prop: String!
}
type U @key(fields: "id") {
id: ID!
field(a: String @fromContext(field: "$context { prop }")): Int!
}
"#;
let subgraph = build_and_expand(schema_str);
let mut errors = MultipleFederationErrors::new();
let context_map = crate::schema::validators::context::validate_context_directives(
subgraph.schema(),
&mut errors,
)
.expect("validates context directives");
validate_from_context_directives(
subgraph.schema(),
subgraph.metadata(),
&context_map,
&mut errors,
)
.expect("validates fromContext directives");
assert!(
errors.errors.is_empty(),
"Should not have validation errors for nullability mismatch when contextual value is non-nullable"
);
}
#[test]
#[ignore] fn test_fromcontext_on_directive_definition() {
let schema_str = r#"
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.8", import: ["@context", "@fromContext", "@key"])
directive @testDirective(
contextArg: String @fromContext(field: "$context prop")
) on FIELD_DEFINITION
type Query {
parent: Parent
}
type Parent @context(name: "context") @key(fields: "id") {
id: ID!
prop: String!
}
type Target @key(fields: "targetId") {
targetId: ID!
value: String
}
"#;
let subgraph = build_and_expand(schema_str);
let mut errors = MultipleFederationErrors::new();
let context_map = crate::schema::validators::context::validate_context_directives(
subgraph.schema(),
&mut errors,
)
.expect("validates context directives");
validate_from_context_directives(
subgraph.schema(),
subgraph.metadata(),
&context_map,
&mut errors,
)
.expect("validates fromContext directives");
assert!(
!errors.errors.is_empty(),
"Should have validation errors for @fromContext on directive definition argument"
);
}
}