Skip to main content

reinhardt_graphql/
schema.rs

1use async_graphql::extensions::Analyzer;
2use async_graphql::{Context, EmptySubscription, ID, Object, Result as GqlResult, Schema};
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::sync::Arc;
6use tokio::sync::RwLock;
7
8#[derive(Debug, thiserror::Error)]
9pub enum GraphQLError {
10	#[error("Schema error: {0}")]
11	Schema(String),
12	#[error("Resolver error: {0}")]
13	Resolver(String),
14	#[error("Not found: {0}")]
15	NotFound(String),
16}
17
18pub type GraphQLResult<T> = Result<T, GraphQLError>;
19
20/// Default maximum query depth limit.
21///
22/// Limits how deeply nested a query can be to prevent resource exhaustion
23/// from deeply nested selections.
24pub const DEFAULT_MAX_QUERY_DEPTH: usize = 10;
25
26/// Default maximum query complexity limit.
27///
28/// Limits total complexity score for a single query to prevent
29/// resource exhaustion from expensive operations.
30pub const DEFAULT_MAX_QUERY_COMPLEXITY: usize = 100;
31
32/// Default maximum query size in bytes.
33///
34/// Prevents excessively large query strings from consuming parsing resources.
35pub const DEFAULT_MAX_QUERY_SIZE: usize = 32_768; // 32 KB
36
37/// Default maximum number of fields in a single query.
38///
39/// Prevents queries that request an excessive number of fields,
40/// which could lead to resource exhaustion.
41pub const DEFAULT_MAX_FIELD_COUNT: usize = 200;
42
43/// Default maximum page size for paginated queries.
44///
45/// Prevents unbounded result sets that could cause memory exhaustion.
46pub const DEFAULT_MAX_PAGE_SIZE: usize = 100;
47
48/// Default page size for paginated queries.
49pub const DEFAULT_PAGE_SIZE: usize = 20;
50
51/// Maximum allowed length for user name input.
52const MAX_NAME_LENGTH: usize = 100;
53
54/// Maximum allowed length for email input.
55const MAX_EMAIL_LENGTH: usize = 254;
56
57/// Configuration for GraphQL query protection limits.
58///
59/// Controls query depth, complexity, size, and field count limits to prevent
60/// denial-of-service attacks through resource exhaustion.
61///
62/// # Examples
63///
64/// ```
65/// use reinhardt_graphql::schema::QueryLimits;
66///
67/// // Use defaults
68/// let limits = QueryLimits::default();
69/// assert_eq!(limits.max_depth, 10);
70/// assert_eq!(limits.max_complexity, 100);
71/// assert_eq!(limits.max_query_size, 32_768);
72/// assert_eq!(limits.max_field_count, 200);
73///
74/// // Custom limits
75/// let limits = QueryLimits::new(15, 200);
76/// assert_eq!(limits.max_depth, 15);
77/// assert_eq!(limits.max_complexity, 200);
78/// ```
79#[derive(Debug, Clone, Copy)]
80pub struct QueryLimits {
81	/// Maximum allowed query depth
82	pub max_depth: usize,
83	/// Maximum allowed query complexity
84	pub max_complexity: usize,
85	/// Maximum allowed query string size in bytes
86	pub max_query_size: usize,
87	/// Maximum allowed number of fields in a query
88	pub max_field_count: usize,
89}
90
91impl QueryLimits {
92	/// Create a new `QueryLimits` with custom depth and complexity values.
93	///
94	/// Uses default values for query size and field count limits.
95	pub fn new(max_depth: usize, max_complexity: usize) -> Self {
96		Self {
97			max_depth,
98			max_complexity,
99			max_query_size: DEFAULT_MAX_QUERY_SIZE,
100			max_field_count: DEFAULT_MAX_FIELD_COUNT,
101		}
102	}
103
104	/// Create a new `QueryLimits` with all values specified.
105	pub fn full(
106		max_depth: usize,
107		max_complexity: usize,
108		max_query_size: usize,
109		max_field_count: usize,
110	) -> Self {
111		Self {
112			max_depth,
113			max_complexity,
114			max_query_size,
115			max_field_count,
116		}
117	}
118}
119
120impl Default for QueryLimits {
121	fn default() -> Self {
122		Self {
123			max_depth: DEFAULT_MAX_QUERY_DEPTH,
124			max_complexity: DEFAULT_MAX_QUERY_COMPLEXITY,
125			max_query_size: DEFAULT_MAX_QUERY_SIZE,
126			max_field_count: DEFAULT_MAX_FIELD_COUNT,
127		}
128	}
129}
130
131/// Validate a GraphQL query string against size and field count limits.
132///
133/// Returns `Ok(())` if the query passes all checks, or an error message
134/// describing which limit was exceeded.
135pub fn validate_query(query: &str, limits: &QueryLimits) -> Result<(), String> {
136	// Check query size
137	if query.len() > limits.max_query_size {
138		return Err(format!(
139			"Query size {} bytes exceeds maximum of {} bytes",
140			query.len(),
141			limits.max_query_size
142		));
143	}
144
145	// Approximate field count by counting field-like tokens
146	// A more accurate count would require parsing, but this provides
147	// a reasonable heuristic for DoS prevention
148	let field_count = count_query_fields(query);
149	if field_count > limits.max_field_count {
150		return Err(format!(
151			"Query field count {} exceeds maximum of {}",
152			field_count, limits.max_field_count
153		));
154	}
155
156	Ok(())
157}
158
159/// Count approximate number of fields in a GraphQL query.
160///
161/// Uses a heuristic approach: counts identifiers that appear after
162/// an opening brace or newline within selection sets.
163fn count_query_fields(query: &str) -> usize {
164	// Simple heuristic: count non-keyword identifiers within braces
165	let mut count = 0;
166	let mut in_string = false;
167	let mut depth: usize = 0;
168
169	for line in query.lines() {
170		let trimmed = line.trim();
171		if trimmed.is_empty() || trimmed.starts_with('#') {
172			continue;
173		}
174
175		for ch in trimmed.chars() {
176			match ch {
177				'"' => in_string = !in_string,
178				'{' if !in_string => depth += 1,
179				'}' if !in_string => depth = depth.saturating_sub(1),
180				_ => {}
181			}
182		}
183
184		// Count field-like lines within selection sets
185		if depth > 0 && !in_string {
186			let field_line = trimmed.trim_start_matches('{').trim();
187			if !field_line.is_empty()
188				&& !field_line.starts_with('}')
189				&& !field_line.starts_with("...")
190				&& !field_line.starts_with("query")
191				&& !field_line.starts_with("mutation")
192				&& !field_line.starts_with("subscription")
193				&& !field_line.starts_with("fragment")
194			{
195				count += 1;
196			}
197		}
198	}
199
200	count
201}
202
203/// Validate input for creating a user.
204///
205/// Enforces:
206/// - Name is non-empty and within length limits
207/// - Name contains only valid characters
208/// - Email is non-empty and within length limits
209/// - Email has a basic valid format
210fn validate_create_user_input(input: &CreateUserInput) -> GqlResult<()> {
211	// Validate name
212	let name = input.name.trim();
213	if name.is_empty() {
214		return Err(async_graphql::Error::new("Name cannot be empty"));
215	}
216	if name.len() > MAX_NAME_LENGTH {
217		return Err(async_graphql::Error::new(format!(
218			"Name exceeds maximum length of {} characters",
219			MAX_NAME_LENGTH
220		)));
221	}
222	if !name
223		.chars()
224		.all(|c| c.is_alphanumeric() || c == '_' || c == '-' || c == ' ' || c == '.')
225	{
226		return Err(async_graphql::Error::new(
227			"Name contains invalid characters (allowed: alphanumeric, spaces, underscores, hyphens, dots)",
228		));
229	}
230
231	// Validate email
232	let email = input.email.trim();
233	if email.is_empty() {
234		return Err(async_graphql::Error::new("Email cannot be empty"));
235	}
236	if email.len() > MAX_EMAIL_LENGTH {
237		return Err(async_graphql::Error::new(format!(
238			"Email exceeds maximum length of {} characters",
239			MAX_EMAIL_LENGTH
240		)));
241	}
242	// Basic email format validation: must contain exactly one @ with parts on both sides
243	let at_count = email.chars().filter(|c| *c == '@').count();
244	if at_count != 1 {
245		return Err(async_graphql::Error::new("Invalid email format"));
246	}
247	let parts: Vec<&str> = email.splitn(2, '@').collect();
248	if parts.len() != 2 || parts[0].is_empty() || parts[1].is_empty() || !parts[1].contains('.') {
249		return Err(async_graphql::Error::new("Invalid email format"));
250	}
251
252	Ok(())
253}
254
255/// Example: User type for GraphQL
256#[derive(Debug, Clone, Serialize, Deserialize)]
257pub struct User {
258	pub id: ID,
259	pub name: String,
260	pub email: String,
261	pub active: bool,
262}
263
264#[Object]
265impl User {
266	async fn id(&self) -> &ID {
267		&self.id
268	}
269
270	async fn name(&self) -> &str {
271		&self.name
272	}
273
274	async fn email(&self) -> &str {
275		&self.email
276	}
277
278	async fn active(&self) -> bool {
279		self.active
280	}
281}
282
283/// User storage (in-memory for example)
284#[derive(Clone)]
285pub struct UserStorage {
286	users: Arc<RwLock<HashMap<String, User>>>,
287}
288
289impl UserStorage {
290	/// Create a new user storage
291	///
292	/// # Examples
293	///
294	/// ```
295	/// use reinhardt_graphql::schema::UserStorage;
296	///
297	/// let storage = UserStorage::new();
298	// Creates a new storage instance with defaults
299	/// ```
300	pub fn new() -> Self {
301		Self {
302			users: Arc::new(RwLock::new(HashMap::new())),
303		}
304	}
305	/// Add a user to storage
306	///
307	pub async fn add_user(&self, user: User) {
308		self.users.write().await.insert(user.id.to_string(), user);
309	}
310	/// Get a user by ID
311	///
312	/// # Examples
313	///
314	/// ```ignore
315	// Retrieve user
316	/// let user = storage.get_user("user-1").await;
317	/// ```
318	pub async fn get_user(&self, id: &str) -> Option<User> {
319		self.users.read().await.get(id).cloned()
320	}
321	/// List all users
322	///
323	/// # Examples
324	///
325	/// ```ignore
326	// List all users
327	/// let users = storage.list_users().await;
328	/// ```
329	pub async fn list_users(&self) -> Vec<User> {
330		self.users.read().await.values().cloned().collect()
331	}
332}
333
334impl Default for UserStorage {
335	fn default() -> Self {
336		Self::new()
337	}
338}
339
340/// GraphQL Query root
341pub struct Query;
342
343#[Object]
344impl Query {
345	async fn user(&self, ctx: &Context<'_>, id: ID) -> GqlResult<Option<User>> {
346		let storage = ctx.data::<UserStorage>()?;
347		Ok(storage.get_user(id.as_ref()).await)
348	}
349
350	/// List users with pagination support.
351	///
352	/// # Arguments
353	///
354	/// * `first` - Maximum number of users to return (default: 20, max: 100)
355	/// * `offset` - Number of users to skip (default: 0)
356	async fn users(
357		&self,
358		ctx: &Context<'_>,
359		first: Option<usize>,
360		offset: Option<usize>,
361	) -> GqlResult<Vec<User>> {
362		let storage = ctx.data::<UserStorage>()?;
363		let limit = first
364			.unwrap_or(DEFAULT_PAGE_SIZE)
365			.min(DEFAULT_MAX_PAGE_SIZE);
366		let skip = offset.unwrap_or(0);
367		let all_users = storage.list_users().await;
368		Ok(all_users.into_iter().skip(skip).take(limit).collect())
369	}
370
371	async fn hello(&self, name: Option<String>) -> String {
372		format!("Hello, {}!", name.unwrap_or_else(|| "World".to_string()))
373	}
374}
375
376/// Input type for creating users
377#[derive(async_graphql::InputObject)]
378pub struct CreateUserInput {
379	pub name: String,
380	pub email: String,
381}
382
383/// GraphQL Mutation root
384pub struct Mutation;
385
386#[Object]
387impl Mutation {
388	async fn create_user(&self, ctx: &Context<'_>, input: CreateUserInput) -> GqlResult<User> {
389		// Validate input before processing
390		validate_create_user_input(&input)?;
391
392		let storage = ctx.data::<UserStorage>()?;
393
394		let user = User {
395			id: ID::from(uuid::Uuid::new_v4().to_string()),
396			name: input.name.trim().to_string(),
397			email: input.email.trim().to_string(),
398			active: true,
399		};
400
401		storage.add_user(user.clone()).await;
402		Ok(user)
403	}
404
405	async fn update_user_status(
406		&self,
407		ctx: &Context<'_>,
408		id: ID,
409		active: bool,
410	) -> GqlResult<Option<User>> {
411		let storage = ctx.data::<UserStorage>()?;
412
413		if let Some(mut user) = storage.get_user(id.as_ref()).await {
414			user.active = active;
415			storage.add_user(user.clone()).await;
416			Ok(Some(user))
417		} else {
418			Ok(None)
419		}
420	}
421}
422
423/// Create GraphQL schema
424pub type AppSchema = Schema<Query, Mutation, EmptySubscription>;
425
426/// Create a GraphQL schema with default query protection limits.
427///
428/// Applies default depth and complexity limits to prevent
429/// resource exhaustion from malicious queries.
430pub fn create_schema(storage: UserStorage) -> AppSchema {
431	create_schema_with_limits(storage, QueryLimits::default())
432}
433
434/// Create a GraphQL schema with custom query protection limits.
435///
436/// Configures depth limit, complexity limit, and the `Analyzer` extension
437/// for query cost analysis.
438///
439/// # Arguments
440///
441/// * `storage` - User data storage
442/// * `limits` - Query protection limits configuration
443pub fn create_schema_with_limits(storage: UserStorage, limits: QueryLimits) -> AppSchema {
444	Schema::build(Query, Mutation, EmptySubscription)
445		.data(storage)
446		.limit_depth(limits.max_depth)
447		.limit_complexity(limits.max_complexity)
448		.extension(Analyzer)
449		.finish()
450}
451
452#[cfg(test)]
453mod tests {
454	use super::*;
455
456	#[tokio::test]
457	async fn test_query_hello() {
458		let storage = UserStorage::new();
459		let schema = create_schema(storage);
460
461		let query = r#"
462            {
463                hello(name: "GraphQL")
464            }
465        "#;
466
467		let result = schema.execute(query).await;
468		let data = result.data.into_json().unwrap();
469		assert_eq!(data["hello"], "Hello, GraphQL!");
470	}
471
472	#[tokio::test]
473	async fn test_mutation_create_user() {
474		let storage = UserStorage::new();
475		let schema = create_schema(storage);
476
477		let query = r#"
478            mutation {
479                createUser(input: { name: "Alice", email: "alice@example.com" }) {
480                    name
481                    email
482                    active
483                }
484            }
485        "#;
486
487		let result = schema.execute(query).await;
488		let data = result.data.into_json().unwrap();
489		assert_eq!(data["createUser"]["name"], "Alice");
490		assert!(data["createUser"]["active"].as_bool().unwrap());
491	}
492
493	#[tokio::test]
494	async fn test_query_user() {
495		let storage = UserStorage::new();
496		let user = User {
497			id: ID::from("test-id-123"),
498			name: "Bob".to_string(),
499			email: "bob@example.com".to_string(),
500			active: true,
501		};
502		storage.add_user(user).await;
503
504		let schema = create_schema(storage);
505
506		let query = r#"
507            {
508                user(id: "test-id-123") {
509                    id
510                    name
511                    email
512                    active
513                }
514            }
515        "#;
516
517		let result = schema.execute(query).await;
518		let data = result.data.into_json().unwrap();
519		assert_eq!(data["user"]["id"], "test-id-123");
520		assert_eq!(data["user"]["name"], "Bob");
521		assert_eq!(data["user"]["email"], "bob@example.com");
522		assert!(data["user"]["active"].as_bool().unwrap());
523	}
524
525	#[tokio::test]
526	async fn test_query_user_not_found() {
527		let storage = UserStorage::new();
528		let schema = create_schema(storage);
529
530		let query = r#"
531            {
532                user(id: "nonexistent-id") {
533                    id
534                    name
535                }
536            }
537        "#;
538
539		let result = schema.execute(query).await;
540		let data = result.data.into_json().unwrap();
541		assert!(data["user"].is_null());
542	}
543
544	#[tokio::test]
545	async fn test_query_users_empty() {
546		let storage = UserStorage::new();
547		let schema = create_schema(storage);
548
549		let query = r#"
550            {
551                users {
552                    id
553                    name
554                }
555            }
556        "#;
557
558		let result = schema.execute(query).await;
559		let data = result.data.into_json().unwrap();
560		assert!(data["users"].is_array());
561		assert_eq!(data["users"].as_array().unwrap().len(), 0);
562	}
563
564	#[tokio::test]
565	async fn test_query_users_multiple() {
566		let storage = UserStorage::new();
567
568		let user1 = User {
569			id: ID::from("1"),
570			name: "Alice".to_string(),
571			email: "alice@example.com".to_string(),
572			active: true,
573		};
574		let user2 = User {
575			id: ID::from("2"),
576			name: "Bob".to_string(),
577			email: "bob@example.com".to_string(),
578			active: false,
579		};
580		let user3 = User {
581			id: ID::from("3"),
582			name: "Charlie".to_string(),
583			email: "charlie@example.com".to_string(),
584			active: true,
585		};
586
587		storage.add_user(user1).await;
588		storage.add_user(user2).await;
589		storage.add_user(user3).await;
590
591		let schema = create_schema(storage);
592
593		let query = r#"
594            {
595                users {
596                    id
597                    name
598                    email
599                    active
600                }
601            }
602        "#;
603
604		let result = schema.execute(query).await;
605		let data = result.data.into_json().unwrap();
606		let users = data["users"].as_array().unwrap();
607		assert_eq!(users.len(), 3);
608
609		// Verify that all users are present
610		let names: Vec<&str> = users.iter().map(|u| u["name"].as_str().unwrap()).collect();
611		assert!(names.contains(&"Alice"));
612		assert!(names.contains(&"Bob"));
613		assert!(names.contains(&"Charlie"));
614	}
615
616	#[tokio::test]
617	async fn test_query_users_pagination_with_first() {
618		// Arrange
619		let storage = UserStorage::new();
620		for i in 0..10 {
621			storage
622				.add_user(User {
623					id: ID::from(format!("user-{}", i)),
624					name: format!("User{}", i),
625					email: format!("user{}@example.com", i),
626					active: true,
627				})
628				.await;
629		}
630		let schema = create_schema(storage);
631
632		// Act: request only 3 users
633		let query = r#"{ users(first: 3) { id } }"#;
634		let result = schema.execute(query).await;
635
636		// Assert
637		assert!(result.errors.is_empty());
638		let data = result.data.into_json().unwrap();
639		let users = data["users"].as_array().unwrap();
640		assert_eq!(users.len(), 3);
641	}
642
643	#[tokio::test]
644	async fn test_query_users_pagination_with_offset() {
645		// Arrange
646		let storage = UserStorage::new();
647		for i in 0..5 {
648			storage
649				.add_user(User {
650					id: ID::from(format!("user-{}", i)),
651					name: format!("User{}", i),
652					email: format!("user{}@example.com", i),
653					active: true,
654				})
655				.await;
656		}
657		let schema = create_schema(storage);
658
659		// Act: skip 3, take 10 -> should get 2
660		let query = r#"{ users(first: 10, offset: 3) { id } }"#;
661		let result = schema.execute(query).await;
662
663		// Assert
664		assert!(result.errors.is_empty());
665		let data = result.data.into_json().unwrap();
666		let users = data["users"].as_array().unwrap();
667		assert_eq!(users.len(), 2);
668	}
669
670	#[tokio::test]
671	async fn test_query_users_enforces_max_page_size() {
672		// Arrange
673		let storage = UserStorage::new();
674		for i in 0..150 {
675			storage
676				.add_user(User {
677					id: ID::from(format!("user-{}", i)),
678					name: format!("User{}", i),
679					email: format!("user{}@example.com", i),
680					active: true,
681				})
682				.await;
683		}
684		let schema = create_schema(storage);
685
686		// Act: request 500 users but max is 100
687		let query = r#"{ users(first: 500) { id } }"#;
688		let result = schema.execute(query).await;
689
690		// Assert: clamped to max page size
691		assert!(result.errors.is_empty());
692		let data = result.data.into_json().unwrap();
693		let users = data["users"].as_array().unwrap();
694		assert_eq!(users.len(), DEFAULT_MAX_PAGE_SIZE);
695	}
696
697	#[tokio::test]
698	async fn test_create_user_validates_empty_name() {
699		// Arrange
700		let storage = UserStorage::new();
701		let schema = create_schema(storage);
702
703		// Act
704		let query = r#"
705			mutation {
706				createUser(input: { name: "   ", email: "test@example.com" }) {
707					id
708				}
709			}
710		"#;
711		let result = schema.execute(query).await;
712
713		// Assert
714		assert!(
715			!result.errors.is_empty(),
716			"expected validation error for empty name"
717		);
718	}
719
720	#[tokio::test]
721	async fn test_create_user_validates_invalid_email() {
722		// Arrange
723		let storage = UserStorage::new();
724		let schema = create_schema(storage);
725
726		// Act
727		let query = r#"
728			mutation {
729				createUser(input: { name: "Alice", email: "not-an-email" }) {
730					id
731				}
732			}
733		"#;
734		let result = schema.execute(query).await;
735
736		// Assert
737		assert!(
738			!result.errors.is_empty(),
739			"expected validation error for invalid email"
740		);
741	}
742
743	#[tokio::test]
744	async fn test_validate_query_rejects_oversized_query() {
745		// Arrange
746		let limits = QueryLimits::full(10, 100, 100, 200); // 100 byte limit
747
748		// Act
749		let long_query = "{ ".to_string() + &"a ".repeat(100) + "}";
750		let result = validate_query(&long_query, &limits);
751
752		// Assert
753		assert!(result.is_err());
754		assert!(result.unwrap_err().contains("exceeds maximum"));
755	}
756
757	#[tokio::test]
758	async fn test_validate_query_accepts_normal_query() {
759		// Arrange
760		let limits = QueryLimits::default();
761
762		// Act
763		let result = validate_query("{ users { id name } }", &limits);
764
765		// Assert
766		assert!(result.is_ok());
767	}
768
769	#[tokio::test]
770	async fn test_mutation_update_user_status() {
771		let storage = UserStorage::new();
772		let user = User {
773			id: ID::from("update-test-id"),
774			name: "David".to_string(),
775			email: "david@example.com".to_string(),
776			active: true,
777		};
778		storage.add_user(user).await;
779
780		let schema = create_schema(storage);
781
782		let query = r#"
783            mutation {
784                updateUserStatus(id: "update-test-id", active: false) {
785                    id
786                    name
787                    active
788                }
789            }
790        "#;
791
792		let result = schema.execute(query).await;
793		let data = result.data.into_json().unwrap();
794		assert_eq!(data["updateUserStatus"]["id"], "update-test-id");
795		assert!(!data["updateUserStatus"]["active"].as_bool().unwrap());
796	}
797
798	#[tokio::test]
799	async fn test_mutation_update_nonexistent_user() {
800		let storage = UserStorage::new();
801		let schema = create_schema(storage);
802
803		let query = r#"
804            mutation {
805                updateUserStatus(id: "does-not-exist", active: false) {
806                    id
807                    name
808                }
809            }
810        "#;
811
812		let result = schema.execute(query).await;
813		let data = result.data.into_json().unwrap();
814		assert!(data["updateUserStatus"].is_null());
815	}
816
817	#[tokio::test]
818	async fn test_user_object_fields() {
819		let user = User {
820			id: ID::from("field-test-id"),
821			name: "Eve".to_string(),
822			email: "eve@example.com".to_string(),
823			active: false,
824		};
825
826		// Test direct field access
827		assert_eq!(user.id.to_string(), "field-test-id");
828		assert_eq!(user.name, "Eve");
829		assert_eq!(user.email, "eve@example.com");
830		assert!(!user.active);
831	}
832
833	#[tokio::test]
834	async fn test_user_storage_add_get() {
835		let storage = UserStorage::new();
836
837		let user = User {
838			id: ID::from("storage-test-1"),
839			name: "Frank".to_string(),
840			email: "frank@example.com".to_string(),
841			active: true,
842		};
843
844		storage.add_user(user.clone()).await;
845
846		let retrieved = storage.get_user("storage-test-1").await;
847		let retrieved = retrieved.unwrap();
848		assert_eq!(retrieved.id.to_string(), "storage-test-1");
849		assert_eq!(retrieved.name, "Frank");
850		assert_eq!(retrieved.email, "frank@example.com");
851		assert!(retrieved.active);
852	}
853
854	#[tokio::test]
855	async fn test_user_storage_list() {
856		let storage = UserStorage::new();
857
858		// Initially empty
859		let users = storage.list_users().await;
860		assert_eq!(users.len(), 0);
861
862		// Add users
863		storage
864			.add_user(User {
865				id: ID::from("list-1"),
866				name: "User1".to_string(),
867				email: "user1@example.com".to_string(),
868				active: true,
869			})
870			.await;
871
872		storage
873			.add_user(User {
874				id: ID::from("list-2"),
875				name: "User2".to_string(),
876				email: "user2@example.com".to_string(),
877				active: false,
878			})
879			.await;
880
881		let users = storage.list_users().await;
882		assert_eq!(users.len(), 2);
883	}
884
885	#[tokio::test]
886	async fn test_create_schema_with_data() {
887		let storage = UserStorage::new();
888		storage
889			.add_user(User {
890				id: ID::from("pre-existing"),
891				name: "PreExisting".to_string(),
892				email: "preexisting@example.com".to_string(),
893				active: true,
894			})
895			.await;
896
897		let schema = create_schema(storage);
898
899		// Verify schema can query pre-existing data
900		let query = r#"
901            {
902                user(id: "pre-existing") {
903                    name
904                }
905            }
906        "#;
907
908		let result = schema.execute(query).await;
909		let data = result.data.into_json().unwrap();
910		assert_eq!(data["user"]["name"], "PreExisting");
911	}
912
913	#[tokio::test]
914	async fn test_graphql_error_types() {
915		let err1 = GraphQLError::Schema("test schema error".to_string());
916		assert!(err1.to_string().contains("Schema error"));
917
918		let err2 = GraphQLError::Resolver("test resolver error".to_string());
919		assert!(err2.to_string().contains("Resolver error"));
920
921		let err3 = GraphQLError::NotFound("test item".to_string());
922		assert!(err3.to_string().contains("Not found"));
923	}
924
925	#[tokio::test]
926	async fn test_query_depth_limit_rejects_deep_query() {
927		// Arrange: depth limit of 1 only allows top-level fields
928		let storage = UserStorage::new();
929		let limits = QueryLimits::new(1, 1000);
930		let schema = create_schema_with_limits(storage, limits);
931
932		// Act: query with nested selection exceeds depth limit of 1
933		let query = r#"
934			{
935				users {
936					name
937				}
938			}
939		"#;
940		let result = schema.execute(query).await;
941
942		// Assert: should produce a depth-limit error
943		assert!(
944			!result.errors.is_empty(),
945			"expected depth limit error but query succeeded"
946		);
947		let error_message = &result.errors[0].message;
948		assert!(
949			error_message.to_lowercase().contains("too deep"),
950			"expected depth-limit message, got: {error_message}"
951		);
952	}
953
954	#[tokio::test]
955	async fn test_query_depth_limit_allows_shallow_query() {
956		// Arrange
957		let storage = UserStorage::new();
958		let limits = QueryLimits::new(10, 1000);
959		let schema = create_schema_with_limits(storage, limits);
960
961		// Act
962		let query = r#"{ hello(name: "Test") }"#;
963		let result = schema.execute(query).await;
964
965		// Assert
966		assert!(
967			result.errors.is_empty(),
968			"expected no errors for shallow query"
969		);
970		let data = result.data.into_json().unwrap();
971		assert_eq!(data["hello"], "Hello, Test!");
972	}
973
974	#[tokio::test]
975	async fn test_query_complexity_limit_rejects_complex_query() {
976		// Arrange: very low complexity limit
977		let storage = UserStorage::new();
978		let limits = QueryLimits::new(100, 1);
979		let schema = create_schema_with_limits(storage, limits);
980
981		// Act: query with multiple fields exceeds complexity of 1
982		let query = r#"
983			{
984				users {
985					id
986					name
987					email
988					active
989				}
990			}
991		"#;
992		let result = schema.execute(query).await;
993
994		// Assert: should produce a complexity-limit error
995		assert!(
996			!result.errors.is_empty(),
997			"expected complexity limit error but query succeeded"
998		);
999		let error_message = &result.errors[0].message;
1000		assert!(
1001			error_message.to_lowercase().contains("complex"),
1002			"expected complexity-limit message, got: {error_message}"
1003		);
1004	}
1005
1006	#[tokio::test]
1007	async fn test_query_limits_default_values() {
1008		// Arrange / Act
1009		let limits = QueryLimits::default();
1010
1011		// Assert
1012		assert_eq!(limits.max_depth, DEFAULT_MAX_QUERY_DEPTH);
1013		assert_eq!(limits.max_complexity, DEFAULT_MAX_QUERY_COMPLEXITY);
1014	}
1015
1016	#[tokio::test]
1017	async fn test_create_schema_with_custom_limits() {
1018		// Arrange
1019		let storage = UserStorage::new();
1020		let limits = QueryLimits::new(20, 500);
1021		let schema = create_schema_with_limits(storage, limits);
1022
1023		// Act: simple query within limits
1024		let query = r#"{ hello }"#;
1025		let result = schema.execute(query).await;
1026
1027		// Assert
1028		assert!(result.errors.is_empty());
1029		let data = result.data.into_json().unwrap();
1030		assert_eq!(data["hello"], "Hello, World!");
1031	}
1032
1033	#[tokio::test]
1034	async fn test_analyzer_extension_present() {
1035		// Arrange
1036		let storage = UserStorage::new();
1037		let schema = create_schema(storage);
1038
1039		// Act: execute query and check for complexity/depth in extensions
1040		let query = r#"{ hello(name: "Analyzer") }"#;
1041		let result = schema.execute(query).await;
1042
1043		// Assert: Analyzer extension adds complexity/depth to response extensions
1044		assert!(result.errors.is_empty());
1045		assert!(
1046			!result.extensions.is_empty(),
1047			"expected Analyzer extension data in response"
1048		);
1049	}
1050}