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 <[email protected]> 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" {
