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

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


The following commit(s) were added to refs/heads/main by this push:
     new 0ff4c42  chore: Align our mock servers impl (#674)
0ff4c42 is described below

commit 0ff4c42d0373eadef7e2b00cf26cbc712202c4bd
Author: Xuanwo <[email protected]>
AuthorDate: Fri Dec 26 21:25:06 2025 +0800

    chore: Align our mock servers impl (#674)
    
    This PR will align our mock servers impl
    
    ---
    
    **Parts of this PR were drafted with assistance from Codex (with
    `gpt-5.2`) and fully reviewed and edited by me. I take full
    responsibility for all changes.**
---
 services/aws-v4/tests/mocks/ecs_mock_server.py     |  8 +++-
 services/azure-storage/src/sign_request.rs         | 54 ++++++++++++++++++++++
 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                    |  2 +
 .../tests/credential_providers/vm_metadata.rs      | 23 +++++++++
 7 files changed, 107 insertions(+), 25 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..ce0ed7f 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,45 @@ 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 +879,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 bcabf08..798474d 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,6 +109,7 @@ 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
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" 
{

Reply via email to