You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@opendal.apache.org by xu...@apache.org on 2025/12/26 13:10:47 UTC

(opendal-reqsign) branch xuanwo/mock-alignment created (now c1ad5a0)

This is an automated email from the ASF dual-hosted git repository.

xuanwo pushed a change to branch xuanwo/mock-alignment
in repository https://gitbox.apache.org/repos/asf/opendal-reqsign.git


      at c1ad5a0  chore: Align our mock servers impl

This branch includes the following new commits:

     new c1ad5a0  chore: Align our mock servers impl

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(opendal-reqsign) 01/01: chore: Align our mock servers impl

Posted by xu...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

xuanwo pushed a commit to branch xuanwo/mock-alignment
in repository https://gitbox.apache.org/repos/asf/opendal-reqsign.git

commit c1ad5a011f0d251e4b99c81a0f029399a1aed83d
Author: Xuanwo <gi...@xuanwo.io>
AuthorDate: Fri Dec 26 21:10:30 2025 +0800

    chore: Align our mock servers impl
---
 services/aws-v4/tests/mocks/ecs_mock_server.py     |  8 +++-
 services/azure-storage/src/sign_request.rs         | 53 ++++++++++++++++++++++
 services/azure-storage/src/user_delegation.rs      |  5 +-
 services/azure-storage/tests/README.md             | 25 +++-------
 services/azure-storage/tests/mocks/imds_mock.py    | 15 ++++--
 services/google/tests/README.md                    |  4 +-
 .../tests/credential_providers/vm_metadata.rs      | 23 ++++++++++
 7 files changed, 107 insertions(+), 26 deletions(-)

diff --git a/services/aws-v4/tests/mocks/ecs_mock_server.py b/services/aws-v4/tests/mocks/ecs_mock_server.py
index 5d8b88e..91d8040 100644
--- a/services/aws-v4/tests/mocks/ecs_mock_server.py
+++ b/services/aws-v4/tests/mocks/ecs_mock_server.py
@@ -15,12 +15,16 @@ class ECSHandler(BaseHTTPRequestHandler):
     def do_GET(self):
         if self.path == '/creds':
             # Return mock credentials
+            last_updated = datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ')
             expiration = (datetime.utcnow() + timedelta(hours=1)).strftime('%Y-%m-%dT%H:%M:%SZ')
             response = {
+                'RoleArn': 'arn:aws:iam::123456789012:role/test-ecs-role',
+                'Type': 'AWS-HMAC',
                 'AccessKeyId': 'AKIAIOSFODNN7EXAMPLE',
                 'SecretAccessKey': 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
                 'Token': 'IQoJb3JpZ2luX2VjEJv//////////wEaCXVzLXdlc3QtMiJGMEQCIDyJl0YXJwU8iBG4gLVxiNJTYfLp3oFxEOpGGHmQuWmFAiBHEK/GkClQFb0aQ/+kOZkzHKVAPItVJW/VEXAMPLE=',
-                'Expiration': expiration
+                'Expiration': expiration,
+                'LastUpdated': last_updated,
             }
             self.send_response(200)
             self.send_header('Content-Type', 'application/json')
@@ -40,4 +44,4 @@ if __name__ == '__main__':
     port = int(sys.argv[1]) if len(sys.argv) > 1 else 51679
     server = HTTPServer(('0.0.0.0', port), ECSHandler)
     print(f'Mock ECS server running on port {port}')
-    server.serve_forever()
\ No newline at end of file
+    server.serve_forever()
diff --git a/services/azure-storage/src/sign_request.rs b/services/azure-storage/src/sign_request.rs
index 75dd702..4fd9b54 100644
--- a/services/azure-storage/src/sign_request.rs
+++ b/services/azure-storage/src/sign_request.rs
@@ -813,6 +813,11 @@ mod tests {
             &self,
             req: http::Request<Bytes>,
         ) -> reqsign_core::Result<http::Response<Bytes>> {
+            if req.method() != http::Method::POST {
+                return Err(reqsign_core::Error::unexpected("unexpected request method")
+                    .with_context(req.method().to_string()));
+            }
+
             let uri = req.uri().to_string();
             if uri
                 != "https://account.blob.core.windows.net/?restype=service&comp=userdelegationkey"
@@ -822,6 +827,44 @@ mod tests {
                 );
             }
 
+            let version = req
+                .headers()
+                .get("x-ms-version")
+                .and_then(|v| v.to_str().ok())
+                .unwrap_or_default()
+                .to_string();
+            if version != "2020-12-06" {
+                return Err(
+                    reqsign_core::Error::unexpected("unexpected x-ms-version header")
+                        .with_context(version),
+                );
+            }
+
+            let date = req
+                .headers()
+                .get("x-ms-date")
+                .and_then(|v| v.to_str().ok())
+                .unwrap_or_default()
+                .to_string();
+            if date != "Tue, 01 Mar 2022 08:12:34 GMT" {
+                return Err(
+                    reqsign_core::Error::unexpected("unexpected x-ms-date header").with_context(date),
+                );
+            }
+
+            let content_type = req
+                .headers()
+                .get(http::header::CONTENT_TYPE)
+                .and_then(|v| v.to_str().ok())
+                .unwrap_or_default()
+                .to_string();
+            if content_type != "application/xml" {
+                return Err(
+                    reqsign_core::Error::unexpected("unexpected content-type header")
+                        .with_context(content_type),
+                );
+            }
+
             let auth = req
                 .headers()
                 .get("authorization")
@@ -835,6 +878,16 @@ mod tests {
                 );
             }
 
+            let body = String::from_utf8_lossy(req.body()).to_string();
+            if !body.contains("<KeyInfo>")
+                || !body.contains("<Start>2022-03-01T08:12:34Z</Start>")
+                || !body.contains("<Expiry>2022-03-01T08:17:34Z</Expiry>")
+            {
+                return Err(
+                    reqsign_core::Error::unexpected("unexpected request body").with_context(body),
+                );
+            }
+
             let body = r#"
 <UserDelegationKey>
   <SignedOid>oid</SignedOid>
diff --git a/services/azure-storage/src/user_delegation.rs b/services/azure-storage/src/user_delegation.rs
index 5c3ccab..3306330 100644
--- a/services/azure-storage/src/user_delegation.rs
+++ b/services/azure-storage/src/user_delegation.rs
@@ -60,8 +60,11 @@ pub(crate) async fn get_user_delegation_key(
         reqsign_core::Error::request_invalid("invalid user delegation key URI").with_source(e)
     })?;
 
+    // Azure Blob Storage expects a KeyInfo payload.
+    //
+    // Reference: https://learn.microsoft.com/en-us/rest/api/storageservices/get-user-delegation-key
     let body = format!(
-        "<UserDelegationKey><SignedStart>{}</SignedStart><SignedExpiry>{}</SignedExpiry></UserDelegationKey>",
+        "<KeyInfo><Start>{}</Start><Expiry>{}</Expiry></KeyInfo>",
         request.start.format_rfc3339_zulu(),
         request.expiry.format_rfc3339_zulu(),
     );
diff --git a/services/azure-storage/tests/README.md b/services/azure-storage/tests/README.md
index 84b636b..875ac4e 100644
--- a/services/azure-storage/tests/README.md
+++ b/services/azure-storage/tests/README.md
@@ -20,8 +20,7 @@ tests/
 │   ├── shared_key.rs       # SharedKey signature tests
 │   └── sas_token.rs        # SAS token handling tests
 └── mocks/                   # Mock servers for testing
-    ├── imds_mock_server.py  # Mock IMDS endpoint
-    └── oauth_mock_server.py # Mock OAuth2 token endpoint
+    └── imds_mock.py         # Mock IMDS endpoint
 ```
 
 ## Running Tests Locally
@@ -109,26 +108,14 @@ The test suite includes mock servers for testing without Azure dependencies:
 
 Simulates the Azure Instance Metadata Service:
 ```bash
-python3 tests/mocks/imds_mock_server.py 8080
+python3 tests/mocks/imds_mock.py 8080
 ```
 
 Then run tests with:
 ```bash
-export REQSIGN_AZURE_STORAGE_TEST_IMDS_MOCK=on
-export AZURE_IMDS_ENDPOINT=http://localhost:8080
-cargo test credential_providers::imds::test_imds_provider_with_mock
-```
-
-### OAuth Mock Server
-
-Simulates Azure AD token endpoints:
-```bash
-python3 tests/mocks/oauth_mock_server.py 8081
-```
-
-Configure with:
-```bash
-export AZURE_AUTHORITY_HOST=http://localhost:8081
+export REQSIGN_AZURE_STORAGE_TEST_IMDS=on
+export AZURE_IMDS_ENDPOINT=http://localhost:8080/metadata/identity/oauth2/token
+cargo test credential_providers::imds:: --no-fail-fast
 ```
 
 ## GitHub Actions
@@ -196,4 +183,4 @@ openssl pkcs12 -info -in $AZURE_CLIENT_CERTIFICATE_PATH
 3. Add environment variable flag if needed
 4. Update GitHub Actions workflow
 5. Update this README
-6. Add secrets to 1Password if required
\ No newline at end of file
+6. Add secrets to 1Password if required
diff --git a/services/azure-storage/tests/mocks/imds_mock.py b/services/azure-storage/tests/mocks/imds_mock.py
index 59d92ad..f20f76c 100644
--- a/services/azure-storage/tests/mocks/imds_mock.py
+++ b/services/azure-storage/tests/mocks/imds_mock.py
@@ -18,13 +18,22 @@ class IMDSHandler(BaseHTTPRequestHandler):
             return
         
         if parsed.path == '/metadata/identity/oauth2/token':
-            # Accept both API versions (2018-02-01 and 2019-08-01)
+            api_version = query.get('api-version', [''])[0]
+            if api_version not in ('2018-02-01', '2019-08-01'):
+                self.send_error(400, "unsupported api-version")
+                return
+
+            resource = query.get('resource', [''])[0]
+            if not resource:
+                self.send_error(400, "resource query required")
+                return
+
             # Mock token response
             token_response = {
                 "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6IjJaUXBKTTBWYmJQWGhzMWpvT1ljYjh0WXhPXyIsImtpZCI6IjJaUXBKTTBWYmJQWGhzMWpvT1ljYjh0WXhPXyJ9.eyJhdWQiOiJodHRwczovL3N0b3JhZ2UuYXp1cmUuY29tLyIsImlzcyI6Imh0dHBzOi8vc3RzLndpbmRvd3MubmV0LzEyMzQ1Njc4LTEyMzQtMTIzNC0xMjM0LTEyMzQ1Njc4OTAxMi8iLCJpYXQiOjE2MzM1MzY0MjIsIm5iZiI6MTYzMzUzNjQyMiwiZXhwIjoxNjMzNjIzMTIyLCJhaW8iOiJFMlpnWUxqL3Y3Ly9kWitQL0JBQSIsImFwcGlkIjoiMTIzNDU2NzgtMTIzNC0xMjM0LTEyMzQtMTIzNDU2Nzg5MDEyIiwiYXBwaWRhY3IiOi [...]
                 "expires_in": "3600",
                 "expires_on": str(int(time.time()) + 3600),
-                "resource": query.get('resource', [''])[0],
+                "resource": resource,
                 "token_type": "Bearer"
             }
             self.send_response(200)
@@ -42,4 +51,4 @@ if __name__ == '__main__':
     port = int(sys.argv[1]) if len(sys.argv) > 1 else 8080
     server = HTTPServer(('', port), IMDSHandler)
     print(f"IMDS mock server started on port {port}")
-    server.serve_forever()
\ No newline at end of file
+    server.serve_forever()
diff --git a/services/google/tests/README.md b/services/google/tests/README.md
index 614665e..90b41fc 100644
--- a/services/google/tests/README.md
+++ b/services/google/tests/README.md
@@ -34,6 +34,7 @@ Tests for request signing functionality:
 - `REQSIGN_GOOGLE_TEST_IMPERSONATION_REAL`: Set to `on` to test with real impersonation credentials
 - `REQSIGN_GOOGLE_TEST_IMPERSONATION_DELEGATES`: Set to `on` to test impersonation with delegation chain
 - `REQSIGN_GOOGLE_TEST_VM_METADATA`: Set to `on` to enable VmMetadataCredentialProvider tests (GCP VMs only)
+- `REQSIGN_GOOGLE_TEST_VM_METADATA_MOCK`: Set to `on` to enable VmMetadataCredentialProvider tests with the local mock server
 
 ### Google Cloud Configuration
 - `GOOGLE_APPLICATION_CREDENTIALS`: Path to credential JSON file (supports all credential types)
@@ -108,8 +109,9 @@ The DefaultCredentialProvider automatically detects and handles all these types.
 ## Notes
 
 - The VmMetadataCredentialProvider tests are disabled by default in CI as they require running on actual GCP VMs
+- CI runs a separate VmMetadataCredentialProvider test via the mock metadata server
 - External Account tests can run in GitHub Actions with proper Workload Identity setup
 - Impersonation tests require proper IAM permissions for the source credentials
 - Tests use real GCS API endpoints to verify signature validity
 - All tests are designed to be idempotent and safe to run repeatedly
-- Some credential provider tests use test data that will fail token exchange - this is expected
\ No newline at end of file
+- Some credential provider tests use test data that will fail token exchange - this is expected
diff --git a/services/google/tests/credential_providers/vm_metadata.rs b/services/google/tests/credential_providers/vm_metadata.rs
index be14e6a..3f13b7d 100644
--- a/services/google/tests/credential_providers/vm_metadata.rs
+++ b/services/google/tests/credential_providers/vm_metadata.rs
@@ -45,6 +45,29 @@ async fn test_vm_metadata_credential_provider() -> Result<()> {
     Ok(())
 }
 
+#[tokio::test]
+async fn test_vm_metadata_credential_provider_with_mock() -> Result<()> {
+    if env::var("REQSIGN_GOOGLE_TEST_VM_METADATA_MOCK").unwrap_or_default() != "on" {
+        warn!("REQSIGN_GOOGLE_TEST_VM_METADATA_MOCK is not set, skipped");
+        return Ok(());
+    }
+
+    let ctx = create_test_context();
+
+    let provider = VmMetadataCredentialProvider::new();
+    let credential = provider
+        .provide_credential(&ctx)
+        .await?
+        .expect("credential must be provided with mock metadata server");
+
+    assert!(credential.has_token(), "Must have access token");
+    assert!(credential.has_valid_token(), "Token must be valid");
+    let token = credential.token.as_ref().unwrap();
+    assert!(!token.access_token.is_empty(), "Token must not be empty");
+
+    Ok(())
+}
+
 #[tokio::test]
 async fn test_vm_metadata_credential_provider_with_scope() -> Result<()> {
     if env::var("REQSIGN_GOOGLE_TEST_VM_METADATA").unwrap_or_default() != "on" {