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 #[cfg(feature = "graphql-grpc")]
657 let builder = crate::GraphQLGrpcService::schema_builder(Query, Mutation, EmptySubscription);
658 #[cfg(not(feature = "graphql-grpc"))]
659 let builder = Schema::build(Query, Mutation, EmptySubscription);
660 builder
661 .data(storage)
662 .limit_depth(limits.max_depth)
663 .limit_complexity(limits.max_complexity)
664 .extension(Analyzer)
665 .finish()
666}
667
668#[cfg(test)]
669mod tests {
670 use super::*;
671
672 #[tokio::test]
673 async fn test_query_hello() {
674 let storage = UserStorage::new();
675 let schema = create_schema(storage);
676
677 let query = r#"
678 {
679 hello(name: "GraphQL")
680 }
681 "#;
682
683 let result = schema.execute(query).await;
684 let data = result.data.into_json().unwrap();
685 assert_eq!(data["hello"], "Hello, GraphQL!");
686 }
687
688 #[tokio::test]
689 async fn test_mutation_create_user() {
690 let storage = UserStorage::new();
691 let schema = create_schema(storage);
692
693 let query = r#"
694 mutation {
695 createUser(input: { name: "Alice", email: "alice@example.com" }) {
696 name
697 email
698 active
699 }
700 }
701 "#;
702
703 let result = schema.execute(query).await;
704 let data = result.data.into_json().unwrap();
705 assert_eq!(data["createUser"]["name"], "Alice");
706 assert!(data["createUser"]["active"].as_bool().unwrap());
707 }
708
709 #[tokio::test]
710 async fn test_query_user() {
711 let storage = UserStorage::new();
712 let user = User {
713 id: ID::from("test-id-123"),
714 name: "Bob".to_string(),
715 email: "bob@example.com".to_string(),
716 active: true,
717 };
718 storage.add_user(user).await;
719
720 let schema = create_schema(storage);
721
722 let query = r#"
723 {
724 user(id: "test-id-123") {
725 id
726 name
727 email
728 active
729 }
730 }
731 "#;
732
733 let result = schema.execute(query).await;
734 let data = result.data.into_json().unwrap();
735 assert_eq!(data["user"]["id"], "test-id-123");
736 assert_eq!(data["user"]["name"], "Bob");
737 assert_eq!(data["user"]["email"], "bob@example.com");
738 assert!(data["user"]["active"].as_bool().unwrap());
739 }
740
741 #[tokio::test]
742 async fn test_query_user_not_found() {
743 let storage = UserStorage::new();
744 let schema = create_schema(storage);
745
746 let query = r#"
747 {
748 user(id: "nonexistent-id") {
749 id
750 name
751 }
752 }
753 "#;
754
755 let result = schema.execute(query).await;
756 let data = result.data.into_json().unwrap();
757 assert!(data["user"].is_null());
758 }
759
760 #[tokio::test]
761 async fn test_query_users_empty() {
762 let storage = UserStorage::new();
763 let schema = create_schema(storage);
764
765 let query = r#"
766 {
767 users {
768 id
769 name
770 }
771 }
772 "#;
773
774 let result = schema.execute(query).await;
775 let data = result.data.into_json().unwrap();
776 assert!(data["users"].is_array());
777 assert_eq!(data["users"].as_array().unwrap().len(), 0);
778 }
779
780 #[tokio::test]
781 async fn test_query_users_multiple() {
782 let storage = UserStorage::new();
783
784 let user1 = User {
785 id: ID::from("1"),
786 name: "Alice".to_string(),
787 email: "alice@example.com".to_string(),
788 active: true,
789 };
790 let user2 = User {
791 id: ID::from("2"),
792 name: "Bob".to_string(),
793 email: "bob@example.com".to_string(),
794 active: false,
795 };
796 let user3 = User {
797 id: ID::from("3"),
798 name: "Charlie".to_string(),
799 email: "charlie@example.com".to_string(),
800 active: true,
801 };
802
803 storage.add_user(user1).await;
804 storage.add_user(user2).await;
805 storage.add_user(user3).await;
806
807 let schema = create_schema(storage);
808
809 let query = r#"
810 {
811 users {
812 id
813 name
814 email
815 active
816 }
817 }
818 "#;
819
820 let result = schema.execute(query).await;
821 let data = result.data.into_json().unwrap();
822 let users = data["users"].as_array().unwrap();
823 assert_eq!(users.len(), 3);
824
825 let names: Vec<&str> = users.iter().map(|u| u["name"].as_str().unwrap()).collect();
827 assert!(names.contains(&"Alice"));
828 assert!(names.contains(&"Bob"));
829 assert!(names.contains(&"Charlie"));
830 }
831
832 #[tokio::test]
833 async fn test_query_users_pagination_with_first() {
834 let storage = UserStorage::new();
836 for i in 0..10 {
837 storage
838 .add_user(User {
839 id: ID::from(format!("user-{}", i)),
840 name: format!("User{}", i),
841 email: format!("user{}@example.com", i),
842 active: true,
843 })
844 .await;
845 }
846 let schema = create_schema(storage);
847
848 let query = r#"{ users(first: 3) { id } }"#;
850 let result = schema.execute(query).await;
851
852 assert!(result.errors.is_empty());
854 let data = result.data.into_json().unwrap();
855 let users = data["users"].as_array().unwrap();
856 assert_eq!(users.len(), 3);
857 }
858
859 #[tokio::test]
860 async fn test_query_users_pagination_with_offset() {
861 let storage = UserStorage::new();
863 for i in 0..5 {
864 storage
865 .add_user(User {
866 id: ID::from(format!("user-{}", i)),
867 name: format!("User{}", i),
868 email: format!("user{}@example.com", i),
869 active: true,
870 })
871 .await;
872 }
873 let schema = create_schema(storage);
874
875 let query = r#"{ users(first: 10, offset: 3) { id } }"#;
877 let result = schema.execute(query).await;
878
879 assert!(result.errors.is_empty());
881 let data = result.data.into_json().unwrap();
882 let users = data["users"].as_array().unwrap();
883 assert_eq!(users.len(), 2);
884 }
885
886 #[tokio::test]
887 async fn test_query_users_enforces_max_page_size() {
888 let storage = UserStorage::new();
890 for i in 0..150 {
891 storage
892 .add_user(User {
893 id: ID::from(format!("user-{}", i)),
894 name: format!("User{}", i),
895 email: format!("user{}@example.com", i),
896 active: true,
897 })
898 .await;
899 }
900 let schema = create_schema(storage);
901
902 let query = r#"{ users(first: 500) { id } }"#;
904 let result = schema.execute(query).await;
905
906 assert!(result.errors.is_empty());
908 let data = result.data.into_json().unwrap();
909 let users = data["users"].as_array().unwrap();
910 assert_eq!(users.len(), DEFAULT_MAX_PAGE_SIZE);
911 }
912
913 #[tokio::test]
914 async fn test_create_user_validates_empty_name() {
915 let storage = UserStorage::new();
917 let schema = create_schema(storage);
918
919 let query = r#"
921 mutation {
922 createUser(input: { name: " ", email: "test@example.com" }) {
923 id
924 }
925 }
926 "#;
927 let result = schema.execute(query).await;
928
929 assert!(
931 !result.errors.is_empty(),
932 "expected validation error for empty name"
933 );
934 }
935
936 #[tokio::test]
937 async fn test_create_user_validates_invalid_email() {
938 let storage = UserStorage::new();
940 let schema = create_schema(storage);
941
942 let query = r#"
944 mutation {
945 createUser(input: { name: "Alice", email: "not-an-email" }) {
946 id
947 }
948 }
949 "#;
950 let result = schema.execute(query).await;
951
952 assert!(
954 !result.errors.is_empty(),
955 "expected validation error for invalid email"
956 );
957 }
958
959 #[tokio::test]
960 async fn test_validate_query_rejects_oversized_query() {
961 let limits = QueryLimits::full(10, 100, 100, 200); let long_query = "{ ".to_string() + &"a ".repeat(100) + "}";
966 let result = validate_query(&long_query, &limits);
967
968 assert!(result.is_err());
970 assert!(result.unwrap_err().contains("exceeds maximum"));
971 }
972
973 #[tokio::test]
974 async fn test_validate_query_accepts_normal_query() {
975 let limits = QueryLimits::default();
977
978 let result = validate_query("{ users { id name } }", &limits);
980
981 assert!(result.is_ok());
983 }
984
985 #[tokio::test]
986 async fn test_mutation_update_user_status() {
987 let storage = UserStorage::new();
988 let user = User {
989 id: ID::from("update-test-id"),
990 name: "David".to_string(),
991 email: "david@example.com".to_string(),
992 active: true,
993 };
994 storage.add_user(user).await;
995
996 let schema = create_schema(storage);
997
998 let query = r#"
999 mutation {
1000 updateUserStatus(id: "update-test-id", active: false) {
1001 id
1002 name
1003 active
1004 }
1005 }
1006 "#;
1007
1008 let result = schema.execute(query).await;
1009 let data = result.data.into_json().unwrap();
1010 assert_eq!(data["updateUserStatus"]["id"], "update-test-id");
1011 assert!(!data["updateUserStatus"]["active"].as_bool().unwrap());
1012 }
1013
1014 #[tokio::test]
1015 async fn test_mutation_update_nonexistent_user() {
1016 let storage = UserStorage::new();
1017 let schema = create_schema(storage);
1018
1019 let query = r#"
1020 mutation {
1021 updateUserStatus(id: "does-not-exist", active: false) {
1022 id
1023 name
1024 }
1025 }
1026 "#;
1027
1028 let result = schema.execute(query).await;
1029 let data = result.data.into_json().unwrap();
1030 assert!(data["updateUserStatus"].is_null());
1031 }
1032
1033 #[tokio::test]
1034 async fn test_user_object_fields() {
1035 let user = User {
1036 id: ID::from("field-test-id"),
1037 name: "Eve".to_string(),
1038 email: "eve@example.com".to_string(),
1039 active: false,
1040 };
1041
1042 assert_eq!(user.id.to_string(), "field-test-id");
1044 assert_eq!(user.name, "Eve");
1045 assert_eq!(user.email, "eve@example.com");
1046 assert!(!user.active);
1047 }
1048
1049 #[tokio::test]
1050 async fn test_user_storage_add_get() {
1051 let storage = UserStorage::new();
1052
1053 let user = User {
1054 id: ID::from("storage-test-1"),
1055 name: "Frank".to_string(),
1056 email: "frank@example.com".to_string(),
1057 active: true,
1058 };
1059
1060 storage.add_user(user.clone()).await;
1061
1062 let retrieved = storage.get_user("storage-test-1").await;
1063 let retrieved = retrieved.unwrap();
1064 assert_eq!(retrieved.id.to_string(), "storage-test-1");
1065 assert_eq!(retrieved.name, "Frank");
1066 assert_eq!(retrieved.email, "frank@example.com");
1067 assert!(retrieved.active);
1068 }
1069
1070 #[tokio::test]
1071 async fn test_user_storage_list() {
1072 let storage = UserStorage::new();
1073
1074 let users = storage.list_users().await;
1076 assert_eq!(users.len(), 0);
1077
1078 storage
1080 .add_user(User {
1081 id: ID::from("list-1"),
1082 name: "User1".to_string(),
1083 email: "user1@example.com".to_string(),
1084 active: true,
1085 })
1086 .await;
1087
1088 storage
1089 .add_user(User {
1090 id: ID::from("list-2"),
1091 name: "User2".to_string(),
1092 email: "user2@example.com".to_string(),
1093 active: false,
1094 })
1095 .await;
1096
1097 let users = storage.list_users().await;
1098 assert_eq!(users.len(), 2);
1099 }
1100
1101 #[tokio::test]
1102 async fn test_create_schema_with_data() {
1103 let storage = UserStorage::new();
1104 storage
1105 .add_user(User {
1106 id: ID::from("pre-existing"),
1107 name: "PreExisting".to_string(),
1108 email: "preexisting@example.com".to_string(),
1109 active: true,
1110 })
1111 .await;
1112
1113 let schema = create_schema(storage);
1114
1115 let query = r#"
1117 {
1118 user(id: "pre-existing") {
1119 name
1120 }
1121 }
1122 "#;
1123
1124 let result = schema.execute(query).await;
1125 let data = result.data.into_json().unwrap();
1126 assert_eq!(data["user"]["name"], "PreExisting");
1127 }
1128
1129 #[tokio::test]
1130 async fn test_graphql_error_types() {
1131 let err1 = GraphQLError::Schema("test schema error".to_string());
1132 assert!(err1.to_string().contains("Schema error"));
1133
1134 let err2 = GraphQLError::Resolver("test resolver error".to_string());
1135 assert!(err2.to_string().contains("Resolver error"));
1136
1137 let err3 = GraphQLError::NotFound("test item".to_string());
1138 assert!(err3.to_string().contains("Not found"));
1139 }
1140
1141 #[tokio::test]
1142 async fn test_query_depth_limit_rejects_deep_query() {
1143 let storage = UserStorage::new();
1145 let limits = QueryLimits::new(1, 1000);
1146 let schema = create_schema_with_limits(storage, limits);
1147
1148 let query = r#"
1150 {
1151 users {
1152 name
1153 }
1154 }
1155 "#;
1156 let result = schema.execute(query).await;
1157
1158 assert!(
1160 !result.errors.is_empty(),
1161 "expected depth limit error but query succeeded"
1162 );
1163 let error_message = &result.errors[0].message;
1164 assert!(
1165 error_message.to_lowercase().contains("too deep"),
1166 "expected depth-limit message, got: {error_message}"
1167 );
1168 }
1169
1170 #[tokio::test]
1171 async fn test_query_depth_limit_allows_shallow_query() {
1172 let storage = UserStorage::new();
1174 let limits = QueryLimits::new(10, 1000);
1175 let schema = create_schema_with_limits(storage, limits);
1176
1177 let query = r#"{ hello(name: "Test") }"#;
1179 let result = schema.execute(query).await;
1180
1181 assert!(
1183 result.errors.is_empty(),
1184 "expected no errors for shallow query"
1185 );
1186 let data = result.data.into_json().unwrap();
1187 assert_eq!(data["hello"], "Hello, Test!");
1188 }
1189
1190 #[tokio::test]
1191 async fn test_query_complexity_limit_rejects_complex_query() {
1192 let storage = UserStorage::new();
1194 let limits = QueryLimits::new(100, 1);
1195 let schema = create_schema_with_limits(storage, limits);
1196
1197 let query = r#"
1199 {
1200 users {
1201 id
1202 name
1203 email
1204 active
1205 }
1206 }
1207 "#;
1208 let result = schema.execute(query).await;
1209
1210 assert!(
1212 !result.errors.is_empty(),
1213 "expected complexity limit error but query succeeded"
1214 );
1215 let error_message = &result.errors[0].message;
1216 assert!(
1217 error_message.to_lowercase().contains("complex"),
1218 "expected complexity-limit message, got: {error_message}"
1219 );
1220 }
1221
1222 #[tokio::test]
1223 async fn test_query_limits_default_values() {
1224 let limits = QueryLimits::default();
1226
1227 assert_eq!(limits.max_depth, DEFAULT_MAX_QUERY_DEPTH);
1229 assert_eq!(limits.max_complexity, DEFAULT_MAX_QUERY_COMPLEXITY);
1230 }
1231
1232 #[tokio::test]
1233 async fn test_create_schema_with_custom_limits() {
1234 let storage = UserStorage::new();
1236 let limits = QueryLimits::new(20, 500);
1237 let schema = create_schema_with_limits(storage, limits);
1238
1239 let query = r#"{ hello }"#;
1241 let result = schema.execute(query).await;
1242
1243 assert!(result.errors.is_empty());
1245 let data = result.data.into_json().unwrap();
1246 assert_eq!(data["hello"], "Hello, World!");
1247 }
1248
1249 #[tokio::test]
1250 async fn test_analyzer_extension_present() {
1251 let storage = UserStorage::new();
1253 let schema = create_schema(storage);
1254
1255 let query = r#"{ hello(name: "Analyzer") }"#;
1257 let result = schema.execute(query).await;
1258
1259 assert!(result.errors.is_empty());
1261 assert!(
1262 !result.extensions.is_empty(),
1263 "expected Analyzer extension data in response"
1264 );
1265 }
1266
1267 #[rstest::rstest]
1268 #[case(
1269 "{\n user(name: \"hello \\\"world\\\"\") {\n id\n }\n}",
1270 2,
1271 "escaped quotes inside string should not affect field count"
1272 )]
1273 #[case(
1274 "{\n user(name: \"hello \\\\\\\"end\") {\n id\n name\n }\n}",
1275 3,
1276 "escaped backslash before quote should correctly toggle string state"
1277 )]
1278 #[case(
1279 "{\n user(name: \"no escapes\") {\n id\n }\n}",
1280 2,
1281 "string without escapes should count fields normally"
1282 )]
1283 #[case(
1284 "{\n user(name: \"a\\\"b\\\"c\") {\n id\n name\n email\n }\n}",
1285 4,
1286 "multiple escaped quotes in a single string literal"
1287 )]
1288 fn test_count_query_fields_with_escaped_strings(
1289 #[case] query: &str,
1290 #[case] expected: usize,
1291 #[case] description: &str,
1292 ) {
1293 let count = count_query_fields(query);
1297
1298 assert_eq!(count, expected, "{}", description);
1300 }
1301
1302 #[rstest::rstest]
1303 #[case(
1304 "{ users { id name email } }",
1305 4,
1306 "parent field plus multiple fields on same line within sub-selection"
1307 )]
1308 #[case(
1309 "{ users { id } }",
1310 2,
1311 "parent field plus single field on same line within sub-selection"
1312 )]
1313 #[case(
1314 "{ users { id name } posts { title body } }",
1315 6,
1316 "two parent fields plus their sub-selection fields on same line"
1317 )]
1318 fn test_count_query_fields_same_line(
1319 #[case] query: &str,
1320 #[case] expected: usize,
1321 #[case] description: &str,
1322 ) {
1323 let count = count_query_fields(query);
1327
1328 assert_eq!(count, expected, "{}", description);
1330 }
1331
1332 #[rstest::rstest]
1333 #[case(
1334 "{ ... on User { id name } }",
1335 2,
1336 "inline fragment type condition should not be counted as a field"
1337 )]
1338 #[case(
1339 "{ users { ... on Admin { role } ... on Member { level } } }",
1340 3,
1341 "multiple inline fragments: users + role + level, type names excluded"
1342 )]
1343 fn test_count_query_fields_inline_fragments(
1344 #[case] query: &str,
1345 #[case] expected: usize,
1346 #[case] description: &str,
1347 ) {
1348 let count = count_query_fields(query);
1352
1353 assert_eq!(count, expected, "{}", description);
1355 }
1356
1357 #[rstest::rstest]
1358 #[case(
1359 "{ user(bio: \"\"\"multi\nline\"\"\") { id } }",
1360 2,
1361 "block string argument content should not be counted as fields"
1362 )]
1363 #[case(
1364 "{ user(desc: \"\"\"has identifier inside\"\"\") { id name } }",
1365 3,
1366 "block string with identifier-like content should not affect field count"
1367 )]
1368 fn test_count_query_fields_block_strings(
1369 #[case] query: &str,
1370 #[case] expected: usize,
1371 #[case] description: &str,
1372 ) {
1373 let count = count_query_fields(query);
1377
1378 assert_eq!(count, expected, "{}", description);
1380 }
1381
1382 #[tokio::test]
1383 async fn test_exceeds_max_chars_short_circuits() {
1384 assert!(!exceeds_max_chars("hello", 5)); assert!(exceeds_max_chars("hello!", 5)); assert!(!exceeds_max_chars("", 0)); assert!(exceeds_max_chars("a", 0)); }
1390
1391 #[tokio::test]
1392 async fn test_create_user_accepts_multibyte_name_within_limit() {
1393 let storage = UserStorage::new();
1395 let schema = create_schema(storage);
1396
1397 let query = r#"
1399 mutation {
1400 createUser(input: { name: "田中太郎", email: "tanaka@example.com" }) {
1401 name
1402 }
1403 }
1404 "#;
1405
1406 let result = schema.execute(query).await;
1408
1409 assert!(
1411 result.errors.is_empty(),
1412 "expected success for multi-byte name within limit, got: {:?}",
1413 result.errors
1414 );
1415 let data = result.data.into_json().unwrap();
1416 assert_eq!(data["createUser"]["name"], "田中太郎");
1417 }
1418
1419 #[tokio::test]
1420 async fn test_create_user_rejects_multibyte_name_over_limit() {
1421 let storage = UserStorage::new();
1423 let schema = create_schema(storage);
1424
1425 let long_name: String = "あ".repeat(MAX_NAME_LENGTH + 1);
1426 let query = format!(
1427 r#"mutation {{ createUser(input: {{ name: "{}", email: "test@example.com" }}) {{ id }} }}"#,
1428 long_name
1429 );
1430
1431 let result = schema.execute(&query).await;
1433
1434 assert!(
1436 !result.errors.is_empty(),
1437 "expected validation error for name exceeding {} characters",
1438 MAX_NAME_LENGTH
1439 );
1440 }
1441
1442 #[tokio::test]
1443 async fn test_create_user_accepts_emoji_name_at_limit() {
1444 let storage = UserStorage::new();
1446 let schema = create_schema(storage);
1447
1448 let name_at_limit: String = "a".repeat(MAX_NAME_LENGTH);
1454 let query = format!(
1455 r#"mutation {{ createUser(input: {{ name: "{}", email: "test@example.com" }}) {{ id }} }}"#,
1456 name_at_limit
1457 );
1458
1459 let result = schema.execute(&query).await;
1461
1462 assert!(
1464 result.errors.is_empty(),
1465 "expected success for name at exactly the limit, got: {:?}",
1466 result.errors
1467 );
1468 }
1469}