lambda_appsync/lib.rs
1#![warn(missing_docs)]
2#![warn(rustdoc::missing_crate_level_docs)]
3#![cfg_attr(docsrs, deny(rustdoc::broken_intra_doc_links))]
4//! This crate provides procedural macros and types for implementing
5//! AWS AppSync Direct Lambda resolvers.
6//!
7//! It helps convert GraphQL schemas into type-safe Rust code with full AWS Lambda runtime support.
8//! The main functionality is provided through the [appsync_lambda_main] and [appsync_operation] macros.
9//!
10//! # Complete Example
11//!
12//! ```no_run
13//! use lambda_appsync::{appsync_lambda_main, appsync_operation, AppsyncError};
14//!
15//! // 1. First define your GraphQL schema (e.g. `schema.graphql`):
16//! //
17//! // type Query {
18//! // players: [Player!]!
19//! // gameStatus: GameStatus!
20//! // }
21//! //
22//! // type Player {
23//! // id: ID!
24//! // name: String!
25//! // team: Team!
26//! // }
27//! //
28//! // enum Team {
29//! // RUST
30//! // PYTHON
31//! // JS
32//! // }
33//! //
34//! // enum GameStatus {
35//! // STARTED
36//! // STOPPED
37//! // }
38//!
39//! // 2. Initialize the Lambda runtime with AWS SDK clients in main.rs:
40//!
41//! // Optional hook for custom request validation/auth
42//! async fn verify_request(
43//! event: &lambda_appsync::AppsyncEvent<Operation>
44//! ) -> Option<lambda_appsync::AppsyncResponse> {
45//! // Return Some(response) to short-circuit normal execution
46//! None
47//! }
48//! // Generate types and runtime setup from schema
49//! appsync_lambda_main!(
50//! "schema.graphql",
51//! // Initialize DynamoDB client if needed
52//! dynamodb() -> aws_sdk_dynamodb::Client,
53//! // Enable validation hook
54//! hook = verify_request,
55//! // Enable batch processing
56//! batch = true
57//! );
58//!
59//! // 3. Implement resolver functions for GraphQL operations:
60//!
61//! #[appsync_operation(query(players))]
62//! async fn get_players() -> Result<Vec<Player>, AppsyncError> {
63//! let client = dynamodb();
64//! todo!()
65//! }
66//!
67//! #[appsync_operation(query(gameStatus))]
68//! async fn get_game_status() -> Result<GameStatus, AppsyncError> {
69//! let client = dynamodb();
70//! todo!()
71//! }
72//! // The macro ensures the function signature matches the GraphQL schema
73//! // and wires everything up to handle AWS AppSync requests automatically
74//! # mod child {fn main() {}}
75//! ```
76
77mod aws_scalars;
78mod id;
79pub mod subscription_filters;
80
81use std::{collections::HashMap, ops::BitOr};
82
83use aws_smithy_types::error::metadata::ProvideErrorMetadata;
84use serde_json::Value;
85
86use serde::{de::DeserializeOwned, Deserialize, Serialize};
87use thiserror::Error;
88
89pub use aws_scalars::{
90 datetime::{AWSDate, AWSDateTime, AWSTime},
91 email::AWSEmail,
92 phone::AWSPhone,
93 timestamp::AWSTimestamp,
94 url::AWSUrl,
95};
96pub use id::ID;
97
98#[doc(inline)]
99pub use lambda_appsync_proc::appsync_lambda_main;
100
101#[doc(inline)]
102pub use lambda_appsync_proc::appsync_operation;
103
104// Re-export crates that are mandatory for the proc_macro to succeed
105pub use aws_config;
106pub use lambda_runtime;
107pub use serde;
108pub use serde_json;
109pub use tokio;
110
111#[cfg(feature = "log")]
112pub use log;
113
114#[cfg(feature = "env_logger")]
115pub use env_logger;
116
117#[cfg(feature = "tracing")]
118pub use tracing;
119#[cfg(feature = "tracing")]
120pub use tracing_subscriber;
121
122/// Authorization strategy for AppSync operations.
123///
124/// It determines whether operations are allowed or denied based on the
125/// authentication context provided by AWS AppSync. It is typically used by AppSync
126/// itself in conjunction with AWS Cognito user pools and usually do not concern
127/// the application code.
128#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)]
129#[serde(rename_all = "UPPERCASE")]
130pub enum AppsyncAuthStrategy {
131 /// Allows the operation by default if no explicit authorizer is associated to the field
132 Allow,
133 /// Denies the operation by default if no explicit authorizer is associated to the field
134 Deny,
135}
136
137/// Identity information for Cognito User Pools authenticated requests.
138#[derive(Debug, Deserialize)]
139#[serde(rename_all = "camelCase")]
140pub struct AppsyncIdentityCognito {
141 /// Unique identifier of the authenticated user/client
142 pub sub: String,
143 /// Username of the authenticated user (from Cognito user pools)
144 pub username: String,
145 /// Identity provider that authenticated the request (e.g. Cognito user pool URL)
146 pub issuer: String,
147 /// Default authorization strategy for the authenticated identity
148 pub default_auth_strategy: AppsyncAuthStrategy,
149 /// Source IP addresses associated with the request
150 pub source_ip: Vec<String>,
151 /// Groups the authenticated user belongs to
152 pub groups: Option<Vec<String>>,
153 /// Additional claims/attributes associated with the identity
154 pub claims: Value,
155}
156
157/// Authentication type in a Cognito Identity Pool
158#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)]
159#[serde(rename_all = "lowercase")]
160pub enum CognitoIdentityAuthType {
161 /// User is authenticated with an identity provider
162 Authenticated,
163 /// User is an unauthenticated guest
164 Unauthenticated,
165}
166
167/// Cognito Identity Pool information for federated IAM authentication
168#[derive(Debug, Deserialize)]
169pub struct CognitoFederatedIdentity {
170 /// Unique identifier assigned to the authenticated/unauthenticated identity
171 /// within the Cognito Identity Pool
172 #[serde(rename = "cognitoIdentityId")]
173 pub identity_id: String,
174 /// Identifier of the Cognito Identity Pool that is being used for federation.
175 /// In the format of region:pool-id
176 #[serde(rename = "cognitoIdentityPoolId")]
177 pub identity_pool_id: String,
178 /// Indicates whether the identity is authenticated with an identity provider
179 /// or is an unauthenticated guest access
180 #[serde(rename = "cognitoIdentityAuthType")]
181 pub auth_type: CognitoIdentityAuthType,
182 /// For authenticated identities, contains information about the identity provider
183 /// used for authentication. Format varies by provider type
184 #[serde(rename = "cognitoIdentityAuthProvider")]
185 pub auth_provider: String,
186}
187
188/// Identity information for IAM-authenticated requests.
189///
190/// Contains AWS IAM-specific authentication details, including optional Cognito
191/// identity pool information when using federated identities.
192#[derive(Debug, Deserialize)]
193#[serde(rename_all = "camelCase")]
194pub struct AppsyncIdentityIam {
195 /// AWS account ID of the caller
196 pub account_id: String,
197 /// Source IP address(es) of the caller
198 pub source_ip: Vec<String>,
199 /// IAM username of the caller
200 pub username: String,
201 /// Full IAM ARN of the caller
202 pub user_arn: String,
203 /// Federated identity information when using Cognito Identity Pools
204 #[serde(flatten)]
205 pub federated_identity: Option<CognitoFederatedIdentity>,
206}
207
208/// Identity information for OIDC-authenticated requests.
209#[derive(Debug, Deserialize)]
210pub struct AppsyncIdentityOidc {
211 /// The issuer of the token
212 pub iss: String,
213 /// The subject (usually the user identifier)
214 pub sub: String,
215 /// Token audience
216 pub aud: String,
217 /// Expiration time
218 pub exp: i64,
219 /// Issued at time
220 pub iat: i64,
221 /// Additional custom claims from the OIDC provider
222 #[serde(flatten)]
223 pub additional_claims: HashMap<String, serde_json::Value>,
224}
225
226/// Identity information for Lambda-authorized requests.
227#[derive(Debug, Deserialize)]
228pub struct AppsyncIdentityLambda {
229 /// Custom resolver context returned by the Lambda authorizer
230 #[serde(rename = "resolverContext")]
231 pub resolver_context: serde_json::Value,
232}
233
234/// Identity information for an AppSync request.
235///
236/// Represents the identity context of the authenticated user/client making the request to
237/// AWS AppSync. This enum corresponds directly to AppSync's authorization types as defined
238/// in the AWS documentation.
239///
240/// Each variant maps to one of the five supported AWS AppSync authorization modes:
241///
242/// - [Cognito](AppsyncIdentity::Cognito): Uses Amazon Cognito User Pools, providing group-based
243/// access control with JWT tokens containing encoded user information like groups and custom claims.
244///
245/// - [Iam](AppsyncIdentity::Iam): Uses AWS IAM roles and policies through AWS Signature Version 4
246/// signing. Can be used either directly with IAM users/roles or through Cognito Identity Pools
247/// for federated access. Enables fine-grained access control through IAM policies.
248///
249/// - [Oidc](AppsyncIdentity::Oidc): OpenID Connect authentication integrating with any
250/// OIDC-compliant provider.
251///
252/// - [Lambda](AppsyncIdentity::Lambda): Custom authorization through an AWS Lambda function
253/// that evaluates each request.
254///
255/// - [ApiKey](AppsyncIdentity::ApiKey): Simple API key-based authentication using keys
256/// generated and managed by AppSync.
257///
258/// The variant is determined by the authorization configuration of your AppSync API and
259/// the authentication credentials provided in the request. Each variant contains structured
260/// information specific to that authentication mode, which can be used in resolvers for
261/// custom authorization logic.
262///
263/// More information can be found in the [AWS documentation](https://docs.aws.amazon.com/appsync/latest/devguide/security-authz.html).
264#[derive(Debug, Deserialize)]
265#[serde(untagged)]
266pub enum AppsyncIdentity {
267 /// Amazon Cognito User Pools authentication
268 Cognito(AppsyncIdentityCognito),
269 /// AWS IAM authentication
270 Iam(AppsyncIdentityIam),
271 /// OpenID Connect authentication
272 Oidc(AppsyncIdentityOidc),
273 /// Lambda authorizer authentication
274 Lambda(AppsyncIdentityLambda),
275 /// API Key authentication (represents null identity in JSON)
276 ApiKey,
277}
278
279/// Metadata about an AppSync GraphQL operation execution.
280///
281/// Contains detailed information about the GraphQL operation being executed,
282/// including the operation type, selected fields, and variables. The type parameter
283/// `O` represents the enum generated by [appsync_lambda_main] that defines all valid
284/// operations for this Lambda resolver.
285#[derive(Debug, Deserialize)]
286#[allow(dead_code)]
287pub struct AppsyncEventInfo<O> {
288 /// The specific GraphQL operation being executed (Query/Mutation)
289 #[serde(flatten)]
290 pub operation: O,
291 /// Raw GraphQL selection set as a string
292 #[serde(rename = "selectionSetGraphQL")]
293 pub selection_set_graphql: String,
294 /// List of selected field paths in the GraphQL query
295 #[serde(rename = "selectionSetList")]
296 pub selection_set_list: Vec<String>,
297 /// Variables passed to the GraphQL operation
298 pub variables: HashMap<String, Value>,
299}
300
301/// Represents a complete AWS AppSync event sent to a Lambda resolver.
302///
303/// Contains all context and data needed to resolve a GraphQL operation, including
304/// authentication details, operation info, and arguments. The generics `O`
305/// must be the Operation enum generated by the [appsync_lambda_main] macro.
306///
307/// # Limitations
308/// - Omits the `stash` field used for pipeline resolvers
309/// - Omits the `prev` field as it's not relevant for direct Lambda resolvers
310#[derive(Debug, Deserialize)]
311#[allow(dead_code)]
312pub struct AppsyncEvent<O> {
313 /// Authentication context
314 pub identity: AppsyncIdentity,
315 /// Raw request context from AppSync
316 pub request: Value,
317 /// Parent field's resolved value in nested resolvers
318 pub source: Value,
319 /// Metadata about the GraphQL operation
320 pub info: AppsyncEventInfo<O>,
321 /// Arguments passed to the GraphQL field
322 #[serde(rename = "arguments")]
323 pub args: Value,
324 // Should never be usefull in a Direct Lambda Invocation context
325 // pub stash: Value,
326 // pub prev: Value,
327}
328
329/// Response structure returned to AWS AppSync from a Lambda resolver.
330///
331/// Can contain either successful data or error information, but not both.
332/// Should be constructed using From implementations for either [Value] (success)
333/// or [AppsyncError] (failure).
334///
335/// # Examples
336/// ```
337/// # use serde_json::json;
338/// # use lambda_appsync::{AppsyncError, AppsyncResponse};
339/// // Success response
340/// let response: AppsyncResponse = json!({ "id": 123 }).into();
341///
342/// // Error response
343/// let error = AppsyncError::new("NotFound", "Resource not found");
344/// let response: AppsyncResponse = error.into();
345/// ```
346#[derive(Debug, Serialize)]
347pub struct AppsyncResponse {
348 data: Option<Value>,
349 #[serde(flatten, skip_serializing_if = "Option::is_none")]
350 error: Option<AppsyncError>,
351}
352
353impl AppsyncResponse {
354 /// Returns an unauthorized error response
355 ///
356 /// This creates a standard unauthorized error response for when a request
357 /// lacks proper authentication.
358 ///
359 /// # Examples
360 /// ```
361 /// # use lambda_appsync::AppsyncResponse;
362 /// let response = AppsyncResponse::unauthorized();
363 /// ```
364 pub fn unauthorized() -> Self {
365 AppsyncError::new("Unauthorized", "This operation cannot be authorized").into()
366 }
367}
368
369impl From<Value> for AppsyncResponse {
370 fn from(value: Value) -> Self {
371 Self {
372 data: Some(value),
373 error: None,
374 }
375 }
376}
377impl From<AppsyncError> for AppsyncResponse {
378 fn from(value: AppsyncError) -> Self {
379 Self {
380 data: None,
381 error: Some(value),
382 }
383 }
384}
385
386/// Error type for AWS AppSync operations
387///
388/// Multiple errors can be combined in one using the pipe operator
389///
390/// # Example
391/// ```
392/// # use lambda_appsync::AppsyncError;
393/// let combined_error = AppsyncError::new("ValidationError", "Email address is invalid") | AppsyncError::new("DatabaseError", "User not found in database");
394/// // error_type: "ValidationError|DatabaseError"
395/// // error_message: "Email address is invalid\nUser not found in database"
396/// ```
397///
398/// Can be created from any AWS SDK error or directly by the user.
399///
400/// # Example
401/// ```
402/// # use lambda_appsync::AppsyncError;
403/// # use aws_sdk_dynamodb::types::AttributeValue;
404/// struct Item {
405/// id: u64,
406/// data: String
407/// }
408/// async fn store_item(item: Item, client: &aws_sdk_dynamodb::Client) -> Result<(), AppsyncError> {
409/// client.put_item()
410/// .table_name("my-table")
411/// .item("id", AttributeValue::N(item.id.to_string()))
412/// .item("data", AttributeValue::S(item.data))
413/// .send()
414/// .await?;
415/// Ok(())
416/// }
417/// ```
418#[derive(Debug, Error, Serialize)]
419#[serde(rename_all = "camelCase")]
420#[error("{error_type}: {error_message}")]
421pub struct AppsyncError {
422 /// The type/category of error that occurred (e.g. "ValidationError", "NotFound", "DatabaseError")
423 pub error_type: String,
424 /// A detailed message describing the specific error condition
425 pub error_message: String,
426}
427impl AppsyncError {
428 /// Creates a new AppSync error with the specified error type and message
429 ///
430 /// # Arguments
431 /// * `error_type` - The type/category of the error (e.g. "ValidationError", "NotFound")
432 /// * `error_message` - A detailed message describing the error
433 ///
434 /// # Example
435 /// ```
436 /// # use lambda_appsync::AppsyncError;
437 /// let error = AppsyncError::new("NotFound", "User with ID 123 not found");
438 /// ```
439 pub fn new(error_type: impl Into<String>, error_message: impl Into<String>) -> Self {
440 AppsyncError {
441 error_type: error_type.into(),
442 error_message: error_message.into(),
443 }
444 }
445}
446impl<T: ProvideErrorMetadata> From<T> for AppsyncError {
447 fn from(value: T) -> Self {
448 let meta = ProvideErrorMetadata::meta(&value);
449 AppsyncError {
450 error_type: meta.code().unwrap_or("Unknown").to_owned(),
451 error_message: meta.message().unwrap_or_default().to_owned(),
452 }
453 }
454}
455
456impl BitOr for AppsyncError {
457 type Output = AppsyncError;
458 fn bitor(self, rhs: Self) -> Self::Output {
459 AppsyncError {
460 error_type: format!("{}|{}", self.error_type, rhs.error_type),
461 error_message: format!("{}\n{}", self.error_message, rhs.error_message),
462 }
463 }
464}
465
466/// Extracts and deserializes a named argument from a JSON Value into the specified type
467///
468/// # Arguments
469/// * `args` - Mutable reference to a JSON Value containing arguments
470/// * `arg_name` - Name of the argument to extract
471///
472/// # Returns
473/// * `Ok(T)` - Successfully deserialized value of type T
474/// * `Err(AppsyncError)` - Error if argument is missing or invalid format
475///
476/// # Examples
477/// ```
478/// # use serde_json::json;
479/// # use lambda_appsync::arg_from_json;
480/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
481/// let mut args = json!({
482/// "userId": "123",
483/// "count": 5
484/// });
485///
486/// // Extract userId as String
487/// let user_id: String = arg_from_json(&mut args, "userId")?;
488/// assert_eq!(user_id, "123");
489///
490/// // Extract count as i32
491/// let count: i32 = arg_from_json(&mut args, "count")?;
492/// assert_eq!(count, 5);
493///
494/// // Error case: invalid type
495/// let result: Result<String, _> = arg_from_json(&mut args, "count");
496/// assert!(result.is_err());
497///
498/// // Error case: missing argument
499/// let result: Result<String, _> = arg_from_json(&mut args, "missing");
500/// assert!(result.is_err());
501/// # Ok(())
502/// # }
503/// ```
504pub fn arg_from_json<T: DeserializeOwned>(
505 args: &mut serde_json::Value,
506 arg_name: &'static str,
507) -> Result<T, AppsyncError> {
508 serde_json::from_value(
509 args.get_mut(arg_name)
510 .unwrap_or(&mut serde_json::Value::Null)
511 .take(),
512 )
513 .map_err(|e| {
514 AppsyncError::new(
515 "InvalidArgs",
516 format!("Argument \"{arg_name}\" is not the expected format ({e})"),
517 )
518 })
519}
520
521/// Serializes a value into a JSON Value for AppSync responses
522///
523/// # Arguments
524/// * `res` - Value to serialize that implements Serialize
525///
526/// # Returns
527/// JSON Value representation of the input
528///
529/// # Panics
530/// Panics if the value cannot be serialized to JSON. This should never happen
531/// for valid AppSync schema objects as generated by the `appsync_lambda_main` proc macro.
532///
533/// # Examples
534/// ```
535/// # use serde::Serialize;
536/// # use serde_json::json;
537/// # use lambda_appsync::res_to_json;
538/// #[derive(Serialize)]
539/// struct User {
540/// id: String,
541/// name: String
542/// }
543///
544/// let user = User {
545/// id: "123".to_string(),
546/// name: "John".to_string()
547/// };
548///
549/// let json = res_to_json(user);
550/// assert_eq!(json, json!({
551/// "id": "123",
552/// "name": "John"
553/// }));
554///
555/// // Simple types also work
556/// let num = res_to_json(42);
557/// assert_eq!(num, json!(42));
558/// ```
559pub fn res_to_json<T: Serialize>(res: T) -> serde_json::Value {
560 serde_json::to_value(res).expect("Appsync schema objects are JSON compatible")
561}
562
563#[cfg(test)]
564mod tests {
565 use super::*;
566 use serde_json::json;
567
568 #[test]
569 fn test_appsync_auth_strategy() {
570 let allow: AppsyncAuthStrategy = serde_json::from_str("\"ALLOW\"").unwrap();
571 let deny: AppsyncAuthStrategy = serde_json::from_str("\"DENY\"").unwrap();
572
573 match allow {
574 AppsyncAuthStrategy::Allow => (),
575 _ => panic!("Expected Allow"),
576 }
577
578 match deny {
579 AppsyncAuthStrategy::Deny => (),
580 _ => panic!("Expected Deny"),
581 }
582 }
583
584 #[test]
585 fn test_appsync_identity_cognito() {
586 let json = json!({
587 "sub": "user123",
588 "username": "testuser",
589 "issuer": "https://cognito-idp.region.amazonaws.com/pool_id",
590 "defaultAuthStrategy": "ALLOW",
591 "sourceIp": ["1.2.3.4"],
592 "groups": ["admin", "users"],
593 "claims": {
594 "email": "user@example.com",
595 "custom:role": "developer"
596 }
597 });
598
599 if let AppsyncIdentity::Cognito(cognito) = serde_json::from_value(json).unwrap() {
600 assert_eq!(cognito.sub, "user123");
601 assert_eq!(cognito.username, "testuser");
602 assert_eq!(
603 cognito.issuer,
604 "https://cognito-idp.region.amazonaws.com/pool_id"
605 );
606 assert_eq!(cognito.default_auth_strategy, AppsyncAuthStrategy::Allow);
607 assert_eq!(cognito.source_ip, vec!["1.2.3.4"]);
608 assert_eq!(
609 cognito.groups,
610 Some(vec!["admin".to_string(), "users".to_string()])
611 );
612 assert_eq!(
613 cognito.claims,
614 json!({
615 "email": "user@example.com",
616 "custom:role": "developer"
617 })
618 );
619 } else {
620 panic!("Expected Cognito variant");
621 }
622 }
623
624 #[test]
625 fn test_appsync_identity_iam() {
626 let json = json!({
627 "accountId": "123456789012",
628 "sourceIp": ["1.2.3.4"],
629 "username": "IAMUser",
630 "userArn": "arn:aws:iam::123456789012:user/IAMUser"
631 });
632
633 if let AppsyncIdentity::Iam(iam) = serde_json::from_value(json).unwrap() {
634 assert_eq!(iam.account_id, "123456789012");
635 assert_eq!(iam.source_ip, vec!["1.2.3.4"]);
636 assert_eq!(iam.username, "IAMUser");
637 assert_eq!(iam.user_arn, "arn:aws:iam::123456789012:user/IAMUser");
638 assert!(iam.federated_identity.is_none());
639 } else {
640 panic!("Expected IAM variant");
641 }
642 }
643
644 #[test]
645 fn test_appsync_identity_iam_with_cognito() {
646 let json = json!({
647 "accountId": "123456789012",
648 "sourceIp": ["1.2.3.4"],
649 "username": "IAMUser",
650 "userArn": "arn:aws:iam::123456789012:user/IAMUser",
651 "cognitoIdentityId": "region:id",
652 "cognitoIdentityPoolId": "region:pool_id",
653 "cognitoIdentityAuthType": "authenticated",
654 "cognitoIdentityAuthProvider": "cognito-idp.region.amazonaws.com/pool_id"
655 });
656
657 if let AppsyncIdentity::Iam(iam) = serde_json::from_value(json).unwrap() {
658 assert_eq!(iam.account_id, "123456789012");
659 assert_eq!(iam.source_ip, vec!["1.2.3.4"]);
660 assert_eq!(iam.username, "IAMUser");
661 assert_eq!(iam.user_arn, "arn:aws:iam::123456789012:user/IAMUser");
662
663 let federated = iam.federated_identity.unwrap();
664 assert_eq!(federated.identity_id, "region:id");
665 assert_eq!(federated.identity_pool_id, "region:pool_id");
666 assert!(matches!(
667 federated.auth_type,
668 CognitoIdentityAuthType::Authenticated
669 ));
670 assert_eq!(
671 federated.auth_provider,
672 "cognito-idp.region.amazonaws.com/pool_id"
673 );
674 } else {
675 panic!("Expected IAM variant");
676 }
677 }
678
679 #[test]
680 fn test_appsync_identity_oidc() {
681 let json = json!({
682 "iss": "https://auth.example.com",
683 "sub": "user123",
684 "aud": "client123",
685 "exp": 1714521210,
686 "iat": 1714517610,
687 "name": "John Doe",
688 "email": "john@example.com",
689 "roles": ["admin"],
690 "org_id": "org123",
691 "custom_claim": "value"
692 });
693
694 if let AppsyncIdentity::Oidc(oidc) = serde_json::from_value(json).unwrap() {
695 assert_eq!(oidc.iss, "https://auth.example.com");
696 assert_eq!(oidc.sub, "user123");
697 assert_eq!(oidc.aud, "client123");
698 assert_eq!(oidc.exp, 1714521210);
699 assert_eq!(oidc.iat, 1714517610);
700 assert_eq!(oidc.additional_claims.get("name").unwrap(), "John Doe");
701 assert_eq!(
702 oidc.additional_claims.get("email").unwrap(),
703 "john@example.com"
704 );
705 assert_eq!(
706 oidc.additional_claims.get("roles").unwrap(),
707 &json!(["admin"])
708 );
709 assert_eq!(oidc.additional_claims.get("org_id").unwrap(), "org123");
710 assert_eq!(oidc.additional_claims.get("custom_claim").unwrap(), "value");
711 } else {
712 panic!("Expected OIDC variant");
713 }
714 }
715
716 #[test]
717 fn test_appsync_identity_lambda() {
718 let json = json!({
719 "resolverContext": {
720 "userId": "user123",
721 "permissions": ["read", "write"],
722 "metadata": {
723 "region": "us-west-2",
724 "environment": "prod"
725 }
726 }
727 });
728
729 if let AppsyncIdentity::Lambda(lambda) = serde_json::from_value(json).unwrap() {
730 assert_eq!(
731 lambda.resolver_context,
732 json!({
733 "userId": "user123",
734 "permissions": ["read", "write"],
735 "metadata": {
736 "region": "us-west-2",
737 "environment": "prod"
738 }
739 })
740 );
741 } else {
742 panic!("Expected Lambda variant");
743 }
744 }
745
746 #[test]
747 fn test_appsync_identity_api_key() {
748 let json = serde_json::Value::Null;
749
750 if let AppsyncIdentity::ApiKey = serde_json::from_value(json).unwrap() {
751 // Test passes if we get the ApiKey variant
752 } else {
753 panic!("Expected ApiKey variant");
754 }
755 }
756
757 #[test]
758 fn test_appsync_response() {
759 let success = AppsyncResponse::from(json!({"field": "value"}));
760 assert!(success.data.is_some());
761 assert!(success.error.is_none());
762
763 let error = AppsyncResponse::from(AppsyncError::new("TestError", "message"));
764 assert!(error.data.is_none());
765 assert!(error.error.is_some());
766 }
767
768 #[test]
769 fn test_appsync_error() {
770 let error = AppsyncError::new("TestError", "message");
771 assert_eq!(error.error_type, "TestError");
772 assert_eq!(error.error_message, "message");
773
774 let error1 = AppsyncError::new("Error1", "msg1");
775 let error2 = AppsyncError::new("Error2", "msg2");
776 let combined = error1 | error2;
777
778 assert_eq!(combined.error_type, "Error1|Error2");
779 assert_eq!(combined.error_message, "msg1\nmsg2");
780 }
781
782 #[test]
783 fn test_arg_from_json() {
784 let mut args = json!({
785 "string": "test",
786 "number": 42,
787 "bool": true
788 });
789
790 let s: String = arg_from_json(&mut args, "string").unwrap();
791 assert_eq!(s, "test");
792
793 let n: i32 = arg_from_json(&mut args, "number").unwrap();
794 assert_eq!(n, 42);
795
796 let b: bool = arg_from_json(&mut args, "bool").unwrap();
797 assert!(b);
798
799 let err: Result<String, _> = arg_from_json(&mut args, "missing");
800 assert!(err.is_err());
801 }
802
803 #[test]
804 fn test_res_to_json() {
805 #[derive(Serialize)]
806 struct Test {
807 field: String,
808 }
809
810 let test = Test {
811 field: "value".to_string(),
812 };
813
814 let json = res_to_json(test);
815 assert_eq!(json, json!({"field": "value"}));
816
817 assert_eq!(res_to_json(42), json!(42));
818 assert_eq!(res_to_json("test"), json!("test"));
819 }
820}