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)]
10pub enum GraphQLError {
11 #[error("Schema error: {0}")]
13 Schema(String),
14 #[error("Resolver error: {0}")]
16 Resolver(String),
17 #[error("Not found: {0}")]
19 NotFound(String),
20}
21
22pub type GraphQLResult<T> = Result<T, GraphQLError>;
24
25pub const DEFAULT_MAX_QUERY_DEPTH: usize = 10;
30
31pub const DEFAULT_MAX_QUERY_COMPLEXITY: usize = 100;
36
37pub const DEFAULT_MAX_QUERY_SIZE: usize = 32_768; pub const DEFAULT_MAX_FIELD_COUNT: usize = 200;
47
48pub const DEFAULT_MAX_PAGE_SIZE: usize = 100;
52
53pub const DEFAULT_PAGE_SIZE: usize = 20;
55
56const MAX_NAME_LENGTH: usize = 100;
58
59const MAX_EMAIL_LENGTH: usize = 254;
61
62fn exceeds_max_chars(s: &str, max: usize) -> bool {
67 s.chars().nth(max).is_some()
68}
69
70#[derive(Debug, Clone, Copy)]
93pub struct QueryLimits {
94 pub max_depth: usize,
96 pub max_complexity: usize,
98 pub max_query_size: usize,
100 pub max_field_count: usize,
102}
103
104impl QueryLimits {
105 pub fn new(max_depth: usize, max_complexity: usize) -> Self {
109 Self {
110 max_depth,
111 max_complexity,
112 max_query_size: DEFAULT_MAX_QUERY_SIZE,
113 max_field_count: DEFAULT_MAX_FIELD_COUNT,
114 }
115 }
116
117 pub fn full(
119 max_depth: usize,
120 max_complexity: usize,
121 max_query_size: usize,
122 max_field_count: usize,
123 ) -> Self {
124 Self {
125 max_depth,
126 max_complexity,
127 max_query_size,
128 max_field_count,
129 }
130 }
131}
132
133impl Default for QueryLimits {
134 fn default() -> Self {
135 Self {
136 max_depth: DEFAULT_MAX_QUERY_DEPTH,
137 max_complexity: DEFAULT_MAX_QUERY_COMPLEXITY,
138 max_query_size: DEFAULT_MAX_QUERY_SIZE,
139 max_field_count: DEFAULT_MAX_FIELD_COUNT,
140 }
141 }
142}
143
144pub fn validate_query(query: &str, limits: &QueryLimits) -> Result<(), String> {
149 if query.len() > limits.max_query_size {
151 return Err(format!(
152 "Query size {} bytes exceeds maximum of {} bytes",
153 query.len(),
154 limits.max_query_size
155 ));
156 }
157
158 let field_count = count_query_fields(query);
162 if field_count > limits.max_field_count {
163 return Err(format!(
164 "Query field count {} exceeds maximum of {}",
165 field_count, limits.max_field_count
166 ));
167 }
168
169 Ok(())
170}
171
172const GRAPHQL_KEYWORDS: &[&str] = &[
174 "query",
175 "mutation",
176 "subscription",
177 "fragment",
178 "on",
179 "true",
180 "false",
181 "null",
182];
183
184fn is_field_identifier(token: &str) -> bool {
190 !token.is_empty()
191 && !token.starts_with("...")
192 && !GRAPHQL_KEYWORDS.contains(&token)
193 && token
194 .chars()
195 .next()
196 .is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
197}
198
199fn count_query_fields(query: &str) -> usize {
211 let mut count = 0;
212 let mut in_string = false;
213 let mut in_block_string = false;
214 let mut depth: usize = 0;
215 let mut token = String::new();
216 let mut in_comment = false;
217 let mut escaped = false;
218 let mut after_on_keyword = false;
221
222 let chars: Vec<char> = query.chars().collect();
223 let len = chars.len();
224 let mut i = 0;
225
226 while i < len {
227 let ch = chars[i];
228
229 if escaped {
230 escaped = false;
231 i += 1;
232 continue;
233 }
234
235 if in_block_string {
237 if ch == '"' && i + 2 < len && chars[i + 1] == '"' && chars[i + 2] == '"' {
238 in_block_string = false;
239 i += 3; } else {
241 i += 1;
242 }
243 continue;
244 }
245
246 if ch == '\n' {
248 in_comment = false;
249 if depth > 0 && !in_string && is_field_identifier(&token) {
251 if after_on_keyword {
252 after_on_keyword = false;
253 } else {
254 count += 1;
255 }
256 }
257 if !is_field_identifier(&token) {
258 after_on_keyword = false;
259 }
260 token.clear();
261 i += 1;
262 continue;
263 }
264
265 if in_comment {
266 i += 1;
267 continue;
268 }
269
270 if in_string {
271 match ch {
272 '\\' => escaped = true,
273 '"' => in_string = false,
274 _ => {}
275 }
276 i += 1;
277 continue;
278 }
279
280 match ch {
281 '#' => {
282 if depth > 0 && is_field_identifier(&token) {
284 if after_on_keyword {
285 after_on_keyword = false;
286 } else {
287 count += 1;
288 }
289 }
290 token.clear();
291 in_comment = true;
292 }
293 '"' => {
294 if i + 2 < len && chars[i + 1] == '"' && chars[i + 2] == '"' {
296 if depth > 0 && is_field_identifier(&token) {
298 if after_on_keyword {
299 after_on_keyword = false;
300 } else {
301 count += 1;
302 }
303 }
304 token.clear();
305 in_block_string = true;
306 i += 3; continue;
308 }
309 if depth > 0 && is_field_identifier(&token) {
311 if after_on_keyword {
312 after_on_keyword = false;
313 } else {
314 count += 1;
315 }
316 }
317 token.clear();
318 in_string = true;
319 }
320 '{' => {
321 if depth > 0 && is_field_identifier(&token) {
323 if after_on_keyword {
324 after_on_keyword = false;
325 } else {
326 count += 1;
327 }
328 }
329 token.clear();
330 depth += 1;
331 }
332 '}' => {
333 if depth > 0 && is_field_identifier(&token) {
335 if after_on_keyword {
336 after_on_keyword = false;
337 } else {
338 count += 1;
339 }
340 }
341 token.clear();
342 depth = depth.saturating_sub(1);
343 }
344 '(' => {
345 if depth > 0 && is_field_identifier(&token) {
347 if after_on_keyword {
348 after_on_keyword = false;
349 } else {
350 count += 1;
351 }
352 }
353 token.clear();
354 }
355 c if c.is_ascii_whitespace() || c == ',' => {
356 if depth > 0 && is_field_identifier(&token) {
358 if after_on_keyword {
359 after_on_keyword = false;
360 } else {
361 if token == "on" {
363 after_on_keyword = true;
364 }
365 count += 1;
366 }
367 } else if token == "on" {
368 after_on_keyword = true;
371 }
372 token.clear();
373 }
374 ')' | ':' | '!' | '@' | '$' | '=' | '|' | '&' => {
375 token.clear();
377 }
378 _ => {
379 token.push(ch);
380 }
381 }
382 i += 1;
383 }
384
385 if depth > 0 && !in_string && is_field_identifier(&token) {
387 if after_on_keyword {
388 } else {
390 count += 1;
391 }
392 }
393
394 count
395}
396
397fn validate_create_user_input(input: &CreateUserInput) -> GqlResult<()> {
405 let name = input.name.trim();
407 if name.is_empty() {
408 return Err(async_graphql::Error::new("Name cannot be empty"));
409 }
410 if exceeds_max_chars(name, MAX_NAME_LENGTH) {
411 return Err(async_graphql::Error::new(format!(
412 "Name exceeds maximum length of {} characters",
413 MAX_NAME_LENGTH
414 )));
415 }
416 if !name
417 .chars()
418 .all(|c| c.is_alphanumeric() || c == '_' || c == '-' || c == ' ' || c == '.')
419 {
420 return Err(async_graphql::Error::new(
421 "Name contains invalid characters (allowed: alphanumeric, spaces, underscores, hyphens, dots)",
422 ));
423 }
424
425 let email = input.email.trim();
427 if email.is_empty() {
428 return Err(async_graphql::Error::new("Email cannot be empty"));
429 }
430 if exceeds_max_chars(email, MAX_EMAIL_LENGTH) {
431 return Err(async_graphql::Error::new(format!(
432 "Email exceeds maximum length of {} characters",
433 MAX_EMAIL_LENGTH
434 )));
435 }
436 let at_count = email.chars().filter(|c| *c == '@').count();
438 if at_count != 1 {
439 return Err(async_graphql::Error::new("Invalid email format"));
440 }
441 let parts: Vec<&str> = email.splitn(2, '@').collect();
442 if parts.len() != 2 || parts[0].is_empty() || parts[1].is_empty() || !parts[1].contains('.') {
443 return Err(async_graphql::Error::new("Invalid email format"));
444 }
445
446 Ok(())
447}
448
449#[derive(Debug, Clone, Serialize, Deserialize)]
451pub struct User {
452 pub id: ID,
454 pub name: String,
456 pub email: String,
458 pub active: bool,
460}
461
462#[Object]
463impl User {
464 async fn id(&self) -> &ID {
465 &self.id
466 }
467
468 async fn name(&self) -> &str {
469 &self.name
470 }
471
472 async fn email(&self) -> &str {
473 &self.email
474 }
475
476 async fn active(&self) -> bool {
477 self.active
478 }
479}
480
481#[derive(Clone)]
483pub struct UserStorage {
484 users: Arc<RwLock<HashMap<String, User>>>,
485}
486
487impl UserStorage {
488 pub fn new() -> Self {
499 Self {
500 users: Arc::new(RwLock::new(HashMap::new())),
501 }
502 }
503 pub async fn add_user(&self, user: User) {
506 self.users.write().await.insert(user.id.to_string(), user);
507 }
508 pub async fn get_user(&self, id: &str) -> Option<User> {
523 self.users.read().await.get(id).cloned()
524 }
525 pub async fn list_users(&self) -> Vec<User> {
540 self.users.read().await.values().cloned().collect()
541 }
542}
543
544impl Default for UserStorage {
545 fn default() -> Self {
546 Self::new()
547 }
548}
549
550pub struct Query;
552
553#[Object]
554impl Query {
555 async fn user(&self, ctx: &Context<'_>, id: ID) -> GqlResult<Option<User>> {
556 let storage = ctx.data::<UserStorage>()?;
557 Ok(storage.get_user(id.as_ref()).await)
558 }
559
560 async fn users(
567 &self,
568 ctx: &Context<'_>,
569 first: Option<usize>,
570 offset: Option<usize>,
571 ) -> GqlResult<Vec<User>> {
572 let storage = ctx.data::<UserStorage>()?;
573 let limit = first
574 .unwrap_or(DEFAULT_PAGE_SIZE)
575 .min(DEFAULT_MAX_PAGE_SIZE);
576 let skip = offset.unwrap_or(0);
577 let all_users = storage.list_users().await;
578 Ok(all_users.into_iter().skip(skip).take(limit).collect())
579 }
580
581 async fn hello(&self, name: Option<String>) -> String {
582 format!("Hello, {}!", name.unwrap_or_else(|| "World".to_string()))
583 }
584}
585
586#[derive(async_graphql::InputObject)]
588pub struct CreateUserInput {
589 pub name: String,
591 pub email: String,
593}
594
595pub struct Mutation;
597
598#[Object]
599impl Mutation {
600 async fn create_user(&self, ctx: &Context<'_>, input: CreateUserInput) -> GqlResult<User> {
601 validate_create_user_input(&input)?;
603
604 let storage = ctx.data::<UserStorage>()?;
605
606 let user = User {
607 id: ID::from(uuid::Uuid::now_v7().to_string()),
608 name: input.name.trim().to_string(),
609 email: input.email.trim().to_string(),
610 active: true,
611 };
612
613 storage.add_user(user.clone()).await;
614 Ok(user)
615 }
616
617 async fn update_user_status(
618 &self,
619 ctx: &Context<'_>,
620 id: ID,
621 active: bool,
622 ) -> GqlResult<Option<User>> {
623 let storage = ctx.data::<UserStorage>()?;
624
625 if let Some(mut user) = storage.get_user(id.as_ref()).await {
626 user.active = active;
627 storage.add_user(user.clone()).await;
628 Ok(Some(user))
629 } else {
630 Ok(None)
631 }
632 }
633}
634
635pub type AppSchema = Schema<Query, Mutation, EmptySubscription>;
637
638pub fn create_schema(storage: UserStorage) -> AppSchema {
643 create_schema_with_limits(storage, QueryLimits::default())
644}
645
646pub fn create_schema_with_limits(storage: UserStorage, limits: QueryLimits) -> AppSchema {
656 Schema::build(Query, Mutation, EmptySubscription)
657 .data(storage)
658 .limit_depth(limits.max_depth)
659 .limit_complexity(limits.max_complexity)
660 .extension(Analyzer)
661 .finish()
662}
663
664#[cfg(test)]
665mod tests {
666 use super::*;
667
668 #[tokio::test]
669 async fn test_query_hello() {
670 let storage = UserStorage::new();
671 let schema = create_schema(storage);
672
673 let query = r#"
674 {
675 hello(name: "GraphQL")
676 }
677 "#;
678
679 let result = schema.execute(query).await;
680 let data = result.data.into_json().unwrap();
681 assert_eq!(data["hello"], "Hello, GraphQL!");
682 }
683
684 #[tokio::test]
685 async fn test_mutation_create_user() {
686 let storage = UserStorage::new();
687 let schema = create_schema(storage);
688
689 let query = r#"
690 mutation {
691 createUser(input: { name: "Alice", email: "alice@example.com" }) {
692 name
693 email
694 active
695 }
696 }
697 "#;
698
699 let result = schema.execute(query).await;
700 let data = result.data.into_json().unwrap();
701 assert_eq!(data["createUser"]["name"], "Alice");
702 assert!(data["createUser"]["active"].as_bool().unwrap());
703 }
704
705 #[tokio::test]
706 async fn test_query_user() {
707 let storage = UserStorage::new();
708 let user = User {
709 id: ID::from("test-id-123"),
710 name: "Bob".to_string(),
711 email: "bob@example.com".to_string(),
712 active: true,
713 };
714 storage.add_user(user).await;
715
716 let schema = create_schema(storage);
717
718 let query = r#"
719 {
720 user(id: "test-id-123") {
721 id
722 name
723 email
724 active
725 }
726 }
727 "#;
728
729 let result = schema.execute(query).await;
730 let data = result.data.into_json().unwrap();
731 assert_eq!(data["user"]["id"], "test-id-123");
732 assert_eq!(data["user"]["name"], "Bob");
733 assert_eq!(data["user"]["email"], "bob@example.com");
734 assert!(data["user"]["active"].as_bool().unwrap());
735 }
736
737 #[tokio::test]
738 async fn test_query_user_not_found() {
739 let storage = UserStorage::new();
740 let schema = create_schema(storage);
741
742 let query = r#"
743 {
744 user(id: "nonexistent-id") {
745 id
746 name
747 }
748 }
749 "#;
750
751 let result = schema.execute(query).await;
752 let data = result.data.into_json().unwrap();
753 assert!(data["user"].is_null());
754 }
755
756 #[tokio::test]
757 async fn test_query_users_empty() {
758 let storage = UserStorage::new();
759 let schema = create_schema(storage);
760
761 let query = r#"
762 {
763 users {
764 id
765 name
766 }
767 }
768 "#;
769
770 let result = schema.execute(query).await;
771 let data = result.data.into_json().unwrap();
772 assert!(data["users"].is_array());
773 assert_eq!(data["users"].as_array().unwrap().len(), 0);
774 }
775
776 #[tokio::test]
777 async fn test_query_users_multiple() {
778 let storage = UserStorage::new();
779
780 let user1 = User {
781 id: ID::from("1"),
782 name: "Alice".to_string(),
783 email: "alice@example.com".to_string(),
784 active: true,
785 };
786 let user2 = User {
787 id: ID::from("2"),
788 name: "Bob".to_string(),
789 email: "bob@example.com".to_string(),
790 active: false,
791 };
792 let user3 = User {
793 id: ID::from("3"),
794 name: "Charlie".to_string(),
795 email: "charlie@example.com".to_string(),
796 active: true,
797 };
798
799 storage.add_user(user1).await;
800 storage.add_user(user2).await;
801 storage.add_user(user3).await;
802
803 let schema = create_schema(storage);
804
805 let query = r#"
806 {
807 users {
808 id
809 name
810 email
811 active
812 }
813 }
814 "#;
815
816 let result = schema.execute(query).await;
817 let data = result.data.into_json().unwrap();
818 let users = data["users"].as_array().unwrap();
819 assert_eq!(users.len(), 3);
820
821 let names: Vec<&str> = users.iter().map(|u| u["name"].as_str().unwrap()).collect();
823 assert!(names.contains(&"Alice"));
824 assert!(names.contains(&"Bob"));
825 assert!(names.contains(&"Charlie"));
826 }
827
828 #[tokio::test]
829 async fn test_query_users_pagination_with_first() {
830 let storage = UserStorage::new();
832 for i in 0..10 {
833 storage
834 .add_user(User {
835 id: ID::from(format!("user-{}", i)),
836 name: format!("User{}", i),
837 email: format!("user{}@example.com", i),
838 active: true,
839 })
840 .await;
841 }
842 let schema = create_schema(storage);
843
844 let query = r#"{ users(first: 3) { id } }"#;
846 let result = schema.execute(query).await;
847
848 assert!(result.errors.is_empty());
850 let data = result.data.into_json().unwrap();
851 let users = data["users"].as_array().unwrap();
852 assert_eq!(users.len(), 3);
853 }
854
855 #[tokio::test]
856 async fn test_query_users_pagination_with_offset() {
857 let storage = UserStorage::new();
859 for i in 0..5 {
860 storage
861 .add_user(User {
862 id: ID::from(format!("user-{}", i)),
863 name: format!("User{}", i),
864 email: format!("user{}@example.com", i),
865 active: true,
866 })
867 .await;
868 }
869 let schema = create_schema(storage);
870
871 let query = r#"{ users(first: 10, offset: 3) { id } }"#;
873 let result = schema.execute(query).await;
874
875 assert!(result.errors.is_empty());
877 let data = result.data.into_json().unwrap();
878 let users = data["users"].as_array().unwrap();
879 assert_eq!(users.len(), 2);
880 }
881
882 #[tokio::test]
883 async fn test_query_users_enforces_max_page_size() {
884 let storage = UserStorage::new();
886 for i in 0..150 {
887 storage
888 .add_user(User {
889 id: ID::from(format!("user-{}", i)),
890 name: format!("User{}", i),
891 email: format!("user{}@example.com", i),
892 active: true,
893 })
894 .await;
895 }
896 let schema = create_schema(storage);
897
898 let query = r#"{ users(first: 500) { id } }"#;
900 let result = schema.execute(query).await;
901
902 assert!(result.errors.is_empty());
904 let data = result.data.into_json().unwrap();
905 let users = data["users"].as_array().unwrap();
906 assert_eq!(users.len(), DEFAULT_MAX_PAGE_SIZE);
907 }
908
909 #[tokio::test]
910 async fn test_create_user_validates_empty_name() {
911 let storage = UserStorage::new();
913 let schema = create_schema(storage);
914
915 let query = r#"
917 mutation {
918 createUser(input: { name: " ", email: "test@example.com" }) {
919 id
920 }
921 }
922 "#;
923 let result = schema.execute(query).await;
924
925 assert!(
927 !result.errors.is_empty(),
928 "expected validation error for empty name"
929 );
930 }
931
932 #[tokio::test]
933 async fn test_create_user_validates_invalid_email() {
934 let storage = UserStorage::new();
936 let schema = create_schema(storage);
937
938 let query = r#"
940 mutation {
941 createUser(input: { name: "Alice", email: "not-an-email" }) {
942 id
943 }
944 }
945 "#;
946 let result = schema.execute(query).await;
947
948 assert!(
950 !result.errors.is_empty(),
951 "expected validation error for invalid email"
952 );
953 }
954
955 #[tokio::test]
956 async fn test_validate_query_rejects_oversized_query() {
957 let limits = QueryLimits::full(10, 100, 100, 200); let long_query = "{ ".to_string() + &"a ".repeat(100) + "}";
962 let result = validate_query(&long_query, &limits);
963
964 assert!(result.is_err());
966 assert!(result.unwrap_err().contains("exceeds maximum"));
967 }
968
969 #[tokio::test]
970 async fn test_validate_query_accepts_normal_query() {
971 let limits = QueryLimits::default();
973
974 let result = validate_query("{ users { id name } }", &limits);
976
977 assert!(result.is_ok());
979 }
980
981 #[tokio::test]
982 async fn test_mutation_update_user_status() {
983 let storage = UserStorage::new();
984 let user = User {
985 id: ID::from("update-test-id"),
986 name: "David".to_string(),
987 email: "david@example.com".to_string(),
988 active: true,
989 };
990 storage.add_user(user).await;
991
992 let schema = create_schema(storage);
993
994 let query = r#"
995 mutation {
996 updateUserStatus(id: "update-test-id", active: false) {
997 id
998 name
999 active
1000 }
1001 }
1002 "#;
1003
1004 let result = schema.execute(query).await;
1005 let data = result.data.into_json().unwrap();
1006 assert_eq!(data["updateUserStatus"]["id"], "update-test-id");
1007 assert!(!data["updateUserStatus"]["active"].as_bool().unwrap());
1008 }
1009
1010 #[tokio::test]
1011 async fn test_mutation_update_nonexistent_user() {
1012 let storage = UserStorage::new();
1013 let schema = create_schema(storage);
1014
1015 let query = r#"
1016 mutation {
1017 updateUserStatus(id: "does-not-exist", active: false) {
1018 id
1019 name
1020 }
1021 }
1022 "#;
1023
1024 let result = schema.execute(query).await;
1025 let data = result.data.into_json().unwrap();
1026 assert!(data["updateUserStatus"].is_null());
1027 }
1028
1029 #[tokio::test]
1030 async fn test_user_object_fields() {
1031 let user = User {
1032 id: ID::from("field-test-id"),
1033 name: "Eve".to_string(),
1034 email: "eve@example.com".to_string(),
1035 active: false,
1036 };
1037
1038 assert_eq!(user.id.to_string(), "field-test-id");
1040 assert_eq!(user.name, "Eve");
1041 assert_eq!(user.email, "eve@example.com");
1042 assert!(!user.active);
1043 }
1044
1045 #[tokio::test]
1046 async fn test_user_storage_add_get() {
1047 let storage = UserStorage::new();
1048
1049 let user = User {
1050 id: ID::from("storage-test-1"),
1051 name: "Frank".to_string(),
1052 email: "frank@example.com".to_string(),
1053 active: true,
1054 };
1055
1056 storage.add_user(user.clone()).await;
1057
1058 let retrieved = storage.get_user("storage-test-1").await;
1059 let retrieved = retrieved.unwrap();
1060 assert_eq!(retrieved.id.to_string(), "storage-test-1");
1061 assert_eq!(retrieved.name, "Frank");
1062 assert_eq!(retrieved.email, "frank@example.com");
1063 assert!(retrieved.active);
1064 }
1065
1066 #[tokio::test]
1067 async fn test_user_storage_list() {
1068 let storage = UserStorage::new();
1069
1070 let users = storage.list_users().await;
1072 assert_eq!(users.len(), 0);
1073
1074 storage
1076 .add_user(User {
1077 id: ID::from("list-1"),
1078 name: "User1".to_string(),
1079 email: "user1@example.com".to_string(),
1080 active: true,
1081 })
1082 .await;
1083
1084 storage
1085 .add_user(User {
1086 id: ID::from("list-2"),
1087 name: "User2".to_string(),
1088 email: "user2@example.com".to_string(),
1089 active: false,
1090 })
1091 .await;
1092
1093 let users = storage.list_users().await;
1094 assert_eq!(users.len(), 2);
1095 }
1096
1097 #[tokio::test]
1098 async fn test_create_schema_with_data() {
1099 let storage = UserStorage::new();
1100 storage
1101 .add_user(User {
1102 id: ID::from("pre-existing"),
1103 name: "PreExisting".to_string(),
1104 email: "preexisting@example.com".to_string(),
1105 active: true,
1106 })
1107 .await;
1108
1109 let schema = create_schema(storage);
1110
1111 let query = r#"
1113 {
1114 user(id: "pre-existing") {
1115 name
1116 }
1117 }
1118 "#;
1119
1120 let result = schema.execute(query).await;
1121 let data = result.data.into_json().unwrap();
1122 assert_eq!(data["user"]["name"], "PreExisting");
1123 }
1124
1125 #[tokio::test]
1126 async fn test_graphql_error_types() {
1127 let err1 = GraphQLError::Schema("test schema error".to_string());
1128 assert!(err1.to_string().contains("Schema error"));
1129
1130 let err2 = GraphQLError::Resolver("test resolver error".to_string());
1131 assert!(err2.to_string().contains("Resolver error"));
1132
1133 let err3 = GraphQLError::NotFound("test item".to_string());
1134 assert!(err3.to_string().contains("Not found"));
1135 }
1136
1137 #[tokio::test]
1138 async fn test_query_depth_limit_rejects_deep_query() {
1139 let storage = UserStorage::new();
1141 let limits = QueryLimits::new(1, 1000);
1142 let schema = create_schema_with_limits(storage, limits);
1143
1144 let query = r#"
1146 {
1147 users {
1148 name
1149 }
1150 }
1151 "#;
1152 let result = schema.execute(query).await;
1153
1154 assert!(
1156 !result.errors.is_empty(),
1157 "expected depth limit error but query succeeded"
1158 );
1159 let error_message = &result.errors[0].message;
1160 assert!(
1161 error_message.to_lowercase().contains("too deep"),
1162 "expected depth-limit message, got: {error_message}"
1163 );
1164 }
1165
1166 #[tokio::test]
1167 async fn test_query_depth_limit_allows_shallow_query() {
1168 let storage = UserStorage::new();
1170 let limits = QueryLimits::new(10, 1000);
1171 let schema = create_schema_with_limits(storage, limits);
1172
1173 let query = r#"{ hello(name: "Test") }"#;
1175 let result = schema.execute(query).await;
1176
1177 assert!(
1179 result.errors.is_empty(),
1180 "expected no errors for shallow query"
1181 );
1182 let data = result.data.into_json().unwrap();
1183 assert_eq!(data["hello"], "Hello, Test!");
1184 }
1185
1186 #[tokio::test]
1187 async fn test_query_complexity_limit_rejects_complex_query() {
1188 let storage = UserStorage::new();
1190 let limits = QueryLimits::new(100, 1);
1191 let schema = create_schema_with_limits(storage, limits);
1192
1193 let query = r#"
1195 {
1196 users {
1197 id
1198 name
1199 email
1200 active
1201 }
1202 }
1203 "#;
1204 let result = schema.execute(query).await;
1205
1206 assert!(
1208 !result.errors.is_empty(),
1209 "expected complexity limit error but query succeeded"
1210 );
1211 let error_message = &result.errors[0].message;
1212 assert!(
1213 error_message.to_lowercase().contains("complex"),
1214 "expected complexity-limit message, got: {error_message}"
1215 );
1216 }
1217
1218 #[tokio::test]
1219 async fn test_query_limits_default_values() {
1220 let limits = QueryLimits::default();
1222
1223 assert_eq!(limits.max_depth, DEFAULT_MAX_QUERY_DEPTH);
1225 assert_eq!(limits.max_complexity, DEFAULT_MAX_QUERY_COMPLEXITY);
1226 }
1227
1228 #[tokio::test]
1229 async fn test_create_schema_with_custom_limits() {
1230 let storage = UserStorage::new();
1232 let limits = QueryLimits::new(20, 500);
1233 let schema = create_schema_with_limits(storage, limits);
1234
1235 let query = r#"{ hello }"#;
1237 let result = schema.execute(query).await;
1238
1239 assert!(result.errors.is_empty());
1241 let data = result.data.into_json().unwrap();
1242 assert_eq!(data["hello"], "Hello, World!");
1243 }
1244
1245 #[tokio::test]
1246 async fn test_analyzer_extension_present() {
1247 let storage = UserStorage::new();
1249 let schema = create_schema(storage);
1250
1251 let query = r#"{ hello(name: "Analyzer") }"#;
1253 let result = schema.execute(query).await;
1254
1255 assert!(result.errors.is_empty());
1257 assert!(
1258 !result.extensions.is_empty(),
1259 "expected Analyzer extension data in response"
1260 );
1261 }
1262
1263 #[rstest::rstest]
1264 #[case(
1265 "{\n user(name: \"hello \\\"world\\\"\") {\n id\n }\n}",
1266 2,
1267 "escaped quotes inside string should not affect field count"
1268 )]
1269 #[case(
1270 "{\n user(name: \"hello \\\\\\\"end\") {\n id\n name\n }\n}",
1271 3,
1272 "escaped backslash before quote should correctly toggle string state"
1273 )]
1274 #[case(
1275 "{\n user(name: \"no escapes\") {\n id\n }\n}",
1276 2,
1277 "string without escapes should count fields normally"
1278 )]
1279 #[case(
1280 "{\n user(name: \"a\\\"b\\\"c\") {\n id\n name\n email\n }\n}",
1281 4,
1282 "multiple escaped quotes in a single string literal"
1283 )]
1284 fn test_count_query_fields_with_escaped_strings(
1285 #[case] query: &str,
1286 #[case] expected: usize,
1287 #[case] description: &str,
1288 ) {
1289 let count = count_query_fields(query);
1293
1294 assert_eq!(count, expected, "{}", description);
1296 }
1297
1298 #[rstest::rstest]
1299 #[case(
1300 "{ users { id name email } }",
1301 4,
1302 "parent field plus multiple fields on same line within sub-selection"
1303 )]
1304 #[case(
1305 "{ users { id } }",
1306 2,
1307 "parent field plus single field on same line within sub-selection"
1308 )]
1309 #[case(
1310 "{ users { id name } posts { title body } }",
1311 6,
1312 "two parent fields plus their sub-selection fields on same line"
1313 )]
1314 fn test_count_query_fields_same_line(
1315 #[case] query: &str,
1316 #[case] expected: usize,
1317 #[case] description: &str,
1318 ) {
1319 let count = count_query_fields(query);
1323
1324 assert_eq!(count, expected, "{}", description);
1326 }
1327
1328 #[rstest::rstest]
1329 #[case(
1330 "{ ... on User { id name } }",
1331 2,
1332 "inline fragment type condition should not be counted as a field"
1333 )]
1334 #[case(
1335 "{ users { ... on Admin { role } ... on Member { level } } }",
1336 3,
1337 "multiple inline fragments: users + role + level, type names excluded"
1338 )]
1339 fn test_count_query_fields_inline_fragments(
1340 #[case] query: &str,
1341 #[case] expected: usize,
1342 #[case] description: &str,
1343 ) {
1344 let count = count_query_fields(query);
1348
1349 assert_eq!(count, expected, "{}", description);
1351 }
1352
1353 #[rstest::rstest]
1354 #[case(
1355 "{ user(bio: \"\"\"multi\nline\"\"\") { id } }",
1356 2,
1357 "block string argument content should not be counted as fields"
1358 )]
1359 #[case(
1360 "{ user(desc: \"\"\"has identifier inside\"\"\") { id name } }",
1361 3,
1362 "block string with identifier-like content should not affect field count"
1363 )]
1364 fn test_count_query_fields_block_strings(
1365 #[case] query: &str,
1366 #[case] expected: usize,
1367 #[case] description: &str,
1368 ) {
1369 let count = count_query_fields(query);
1373
1374 assert_eq!(count, expected, "{}", description);
1376 }
1377
1378 #[tokio::test]
1379 async fn test_exceeds_max_chars_short_circuits() {
1380 assert!(!exceeds_max_chars("hello", 5)); assert!(exceeds_max_chars("hello!", 5)); assert!(!exceeds_max_chars("", 0)); assert!(exceeds_max_chars("a", 0)); }
1386
1387 #[tokio::test]
1388 async fn test_create_user_accepts_multibyte_name_within_limit() {
1389 let storage = UserStorage::new();
1391 let schema = create_schema(storage);
1392
1393 let query = r#"
1395 mutation {
1396 createUser(input: { name: "田中太郎", email: "tanaka@example.com" }) {
1397 name
1398 }
1399 }
1400 "#;
1401
1402 let result = schema.execute(query).await;
1404
1405 assert!(
1407 result.errors.is_empty(),
1408 "expected success for multi-byte name within limit, got: {:?}",
1409 result.errors
1410 );
1411 let data = result.data.into_json().unwrap();
1412 assert_eq!(data["createUser"]["name"], "田中太郎");
1413 }
1414
1415 #[tokio::test]
1416 async fn test_create_user_rejects_multibyte_name_over_limit() {
1417 let storage = UserStorage::new();
1419 let schema = create_schema(storage);
1420
1421 let long_name: String = "あ".repeat(MAX_NAME_LENGTH + 1);
1422 let query = format!(
1423 r#"mutation {{ createUser(input: {{ name: "{}", email: "test@example.com" }}) {{ id }} }}"#,
1424 long_name
1425 );
1426
1427 let result = schema.execute(&query).await;
1429
1430 assert!(
1432 !result.errors.is_empty(),
1433 "expected validation error for name exceeding {} characters",
1434 MAX_NAME_LENGTH
1435 );
1436 }
1437
1438 #[tokio::test]
1439 async fn test_create_user_accepts_emoji_name_at_limit() {
1440 let storage = UserStorage::new();
1442 let schema = create_schema(storage);
1443
1444 let name_at_limit: String = "a".repeat(MAX_NAME_LENGTH);
1450 let query = format!(
1451 r#"mutation {{ createUser(input: {{ name: "{}", email: "test@example.com" }}) {{ id }} }}"#,
1452 name_at_limit
1453 );
1454
1455 let result = schema.execute(&query).await;
1457
1458 assert!(
1460 result.errors.is_empty(),
1461 "expected success for name at exactly the limit, got: {:?}",
1462 result.errors
1463 );
1464 }
1465}